初始化天卡开放平台后端项目
包含完整功能模块: - 设备激活(直销激活码模式 + 代销免激活码模式) - 微信支付 + 天卡订单 - 天卡池管理 + 异步橘视厅SDK激活 - 第三方合作伙伴接口(代销模式) - 签名鉴权(appKey/appSecret + nonce防重放) - 天卡消耗(事务 + 幂等 + 并发控制) - 媒资接口(三级授权过滤) - 审计日志 - 定价策略(3层:门店 > 渠道 > 平台) - 分账(微信分账 + 分润规则) - Docker部署(含Redis容器)
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
package io.as.modules.daycard.constants;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* 天卡业务相关常量
|
||||
*/
|
||||
public final class DayCardConstants {
|
||||
|
||||
private DayCardConstants() {
|
||||
}
|
||||
|
||||
// ==================== 订单状态 ====================
|
||||
/** 已创建 */
|
||||
public static final String ORDER_STATUS_CREATED = "PENDING";
|
||||
/** 支付中 */
|
||||
public static final String ORDER_STATUS_PAYING = "PAYING";
|
||||
/** 已支付 */
|
||||
public static final String ORDER_STATUS_PAID = "PAID";
|
||||
/** 支付失败 */
|
||||
public static final String ORDER_STATUS_PAY_FAILED = "PAY_FAILED";
|
||||
/** 已取消 */
|
||||
public static final String ORDER_STATUS_CANCELLED = "CANCELLED";
|
||||
/** 退款中 */
|
||||
public static final String ORDER_STATUS_REFUNDING = "REFUNDING";
|
||||
|
||||
// ==================== 天卡池状态 ====================
|
||||
/** 未使用 */
|
||||
public static final String POOL_STATUS_UNUSED = "UNUSED";
|
||||
/** 已预留 */
|
||||
public static final String POOL_STATUS_RESERVED = "RESERVED";
|
||||
/** 已使用 */
|
||||
public static final String POOL_STATUS_USED = "USED";
|
||||
/** 可复用(已解绑,橘视厅权益仍有效) */
|
||||
public static final String POOL_STATUS_REUSABLE = "REUSABLE";
|
||||
|
||||
// ==================== 导入批次状态 ====================
|
||||
/** 处理中 */
|
||||
public static final String BATCH_STATUS_PROCESSING = "PROCESSING";
|
||||
/** 成功 */
|
||||
public static final String BATCH_STATUS_SUCCEEDED = "SUCCEEDED";
|
||||
/** 部分失败 */
|
||||
public static final String BATCH_STATUS_PARTIAL_FAILED = "PARTIAL_FAILED";
|
||||
/** 失败 */
|
||||
public static final String BATCH_STATUS_FAILED = "FAILED";
|
||||
|
||||
// ==================== 激活状态 ====================
|
||||
/** 待激活 */
|
||||
public static final String ACTIVATE_STATUS_PENDING = "PENDING";
|
||||
/** 激活成功 */
|
||||
public static final String ACTIVATE_STATUS_SUCCESS = "SUCCESS";
|
||||
/** 激活失败 */
|
||||
public static final String ACTIVATE_STATUS_FAILED = "FAILED";
|
||||
|
||||
// ==================== 卡类型 ====================
|
||||
/** 天卡 */
|
||||
public static final String CARD_TYPE_DAY = "DAY";
|
||||
|
||||
// ==================== Redis Key 前缀 ====================
|
||||
/** 设备防重锁前缀 */
|
||||
public static final String REDIS_LOCK_PREFIX = "daycard:lock:device:";
|
||||
/** 订单前缀 */
|
||||
public static final String REDIS_ORDER_PREFIX = "daycard:order:";
|
||||
/** 设备订单前缀 */
|
||||
public static final String REDIS_DEVICE_ORDER_PREFIX = "daycard:device:";
|
||||
/** 代销天卡消耗防重锁前缀(按设备标识加锁) */
|
||||
public static final String REDIS_PARTNER_CONSUME_LOCK_PREFIX = "daycard:partner:consume:";
|
||||
/** 第三方激活Token缓存前缀(按thirdCardNo缓存) */
|
||||
public static final String REDIS_THIRD_PARTY_TOKEN_PREFIX = "api:thirdparty:activate:";
|
||||
/** 第三方激活任务锁前缀(按poolId加锁,防止重复激活) */
|
||||
public static final String REDIS_THIRD_PARTY_ACTIVATE_LOCK_PREFIX = "api:thirdparty:activate:lock:";
|
||||
|
||||
// ==================== 删除标记 ====================
|
||||
/** 未删除 */
|
||||
public static final String DELETE_FLAG_UNDEL = "UNDEL";
|
||||
/** 已删除 */
|
||||
public static final String DELETE_FLAG_DEL = "DEL";
|
||||
|
||||
// ==================== 分润规则范围 ====================
|
||||
/** 全局 */
|
||||
public static final String SCOPE_GLOBAL = "GLOBAL";
|
||||
/** 渠道 */
|
||||
public static final String SCOPE_CHANNEL = "CHANNEL";
|
||||
/** 代理商 */
|
||||
public static final String SCOPE_AGENT = "AGENT";
|
||||
/** 门店 */
|
||||
public static final String SCOPE_STORE = "STORE";
|
||||
|
||||
// ==================== 分润规则状态 ====================
|
||||
/** 启用 */
|
||||
public static final String PROFIT_RULE_STATUS_ACTIVE = "ACTIVE";
|
||||
/** 禁用 */
|
||||
public static final String PROFIT_RULE_STATUS_DISABLED = "DISABLED";
|
||||
|
||||
// ==================== 分润类型 ====================
|
||||
/** 按比例分润 */
|
||||
public static final String SHARE_TYPE_RATIO = "RATIO";
|
||||
/** 固定金额分润 */
|
||||
public static final String SHARE_TYPE_FIXED = "FIXED";
|
||||
|
||||
// ==================== 分账单状态 ====================
|
||||
/** 待处理 */
|
||||
public static final String SETTLE_STATUS_PENDING = "PENDING";
|
||||
/** 处理中 */
|
||||
public static final String SETTLE_STATUS_PROCESSING = "PROCESSING";
|
||||
/** 成功 */
|
||||
public static final String SETTLE_STATUS_SUCCESS = "SUCCESS";
|
||||
/** 失败 */
|
||||
public static final String SETTLE_STATUS_FAILED = "FAILED";
|
||||
/** 部分成功 */
|
||||
public static final String SETTLE_STATUS_PARTIAL = "PARTIAL";
|
||||
|
||||
// ==================== 接收方绑定状态 ====================
|
||||
/** 待绑定 */
|
||||
public static final String BIND_STATUS_PENDING = "PENDING";
|
||||
/** 已绑定 */
|
||||
public static final String BIND_STATUS_ACTIVE = "ACTIVE";
|
||||
/** 绑定失败 */
|
||||
public static final String BIND_STATUS_FAILED = "FAILED";
|
||||
|
||||
// ==================== 分账接收方类型 ====================
|
||||
/** 商户号 */
|
||||
public static final String RECEIVER_TYPE_MERCHANT_ID = "MERCHANT_ID";
|
||||
/** 个人OpenID */
|
||||
public static final String RECEIVER_TYPE_PERSONAL_OPENID = "PERSONAL_OPENID";
|
||||
|
||||
// ==================== 组织角色 ====================
|
||||
/** 平台 */
|
||||
public static final String ORG_ROLE_PLATFORM = "PLATFORM";
|
||||
/** 渠道 */
|
||||
public static final String ORG_ROLE_CHANNEL = "CHANNEL";
|
||||
/** 代理 */
|
||||
public static final String ORG_ROLE_AGENT = "AGENT";
|
||||
/** 门店 */
|
||||
public static final String ORG_ROLE_STORE = "STORE";
|
||||
|
||||
// ==================== 微信绑定状态 ====================
|
||||
/** 已绑定 */
|
||||
public static final String WX_BIND_STATUS_BINDED = "BINDED";
|
||||
/** 未绑定 */
|
||||
public static final String WX_BIND_STATUS_UNBOUND = "UNBOUND";
|
||||
|
||||
// ==================== 分润比例上限 ====================
|
||||
/** 分润比例上限:30% */
|
||||
public static final BigDecimal SHARE_RATIO_LIMIT = new BigDecimal("0.3000");
|
||||
/** 非平台方分润比例之和上限:30% */
|
||||
public static final double PROFIT_SHARE_LIMIT = 30.0;
|
||||
|
||||
// ==================== 退款状态 ====================
|
||||
/** 退款待处理 */
|
||||
public static final String REFUND_STATUS_PENDING = "PENDING";
|
||||
/** 退款处理中 */
|
||||
public static final String REFUND_STATUS_PROCESSING = "PROCESSING";
|
||||
/** 退款成功 */
|
||||
public static final String REFUND_STATUS_SUCCESS = "SUCCESS";
|
||||
/** 退款失败 */
|
||||
public static final String REFUND_STATUS_FAILED = "FAILED";
|
||||
/** 退款关闭 */
|
||||
public static final String REFUND_STATUS_CLOSED = "CLOSED";
|
||||
|
||||
// ==================== 支付渠道 ====================
|
||||
/** Mock支付 */
|
||||
public static final String PAY_CHANNEL_MOCK = "MOCK";
|
||||
/** 微信支付 */
|
||||
public static final String PAY_CHANNEL_WECHAT = "WECHAT";
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
package io.as.modules.daycard.controller;
|
||||
|
||||
import io.as.common.annotation.AuditAction;
|
||||
import io.as.common.exception.ASException;
|
||||
import io.as.common.utils.R;
|
||||
import io.as.modules.daycard.entity.DayCardActivationCodeEntity;
|
||||
import io.as.modules.daycard.service.DayCardActivationCodeService;
|
||||
import io.as.modules.sys.entity.SysOrgEntity;
|
||||
import io.as.modules.sys.service.SysOrgService;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import io.swagger.annotations.ApiParam;
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 天卡设备激活码管理(管理后台用)
|
||||
* <p>管理后台按门店生成激活码,App 端 init 时输入激活码完成设备注册绑定</p>
|
||||
*/
|
||||
@Api(tags = "天卡设备激活码")
|
||||
@RestController
|
||||
@RequestMapping("/manage/daycard/activation-code")
|
||||
public class DayCardActivationCodeController {
|
||||
|
||||
@Autowired
|
||||
private DayCardActivationCodeService dayCardActivationCodeService;
|
||||
|
||||
@Autowired
|
||||
private SysOrgService sysOrgService;
|
||||
|
||||
/**
|
||||
* 按门店查询激活码列表
|
||||
*/
|
||||
@ApiOperation("按门店查询激活码列表")
|
||||
@GetMapping("/list/{storeOrgId}")
|
||||
public R listByStore(@PathVariable("storeOrgId") Long storeOrgId) {
|
||||
List<DayCardActivationCodeEntity> list = dayCardActivationCodeService.listByStoreOrgId(storeOrgId);
|
||||
List<Map<String, Object>> result = new ArrayList<>();
|
||||
for (DayCardActivationCodeEntity entity : list) {
|
||||
result.add(toMap(entity));
|
||||
}
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("list", result);
|
||||
return R.dataSuccess(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 为门店生成激活码(每门店一个,已存在则返回已有的)
|
||||
*/
|
||||
@ApiOperation("为门店生成激活码")
|
||||
@AuditAction(value = "ACTCODE_GENERATE", description = "生成门店激活码")
|
||||
@PostMapping("/generate")
|
||||
public R generate(@RequestBody GenerateRequest request) {
|
||||
if (request.getStoreOrgId() == null) {
|
||||
throw new ASException("门店组织ID不能为空");
|
||||
}
|
||||
// 校验门店存在
|
||||
SysOrgEntity storeOrg = sysOrgService.getById(request.getStoreOrgId());
|
||||
if (storeOrg == null) {
|
||||
throw new ASException("门店不存在");
|
||||
}
|
||||
// 从门店 mpath 解析渠道
|
||||
Long channelOrgId = resolveChannelOrgId(storeOrg);
|
||||
DayCardActivationCodeEntity entity = dayCardActivationCodeService.generateForStore(
|
||||
request.getStoreOrgId(), channelOrgId, request.getRemark());
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("data", toMap(entity));
|
||||
return R.dataSuccess(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新生成激活码 code(旧 code 失效)
|
||||
*/
|
||||
@ApiOperation("重新生成激活码")
|
||||
@AuditAction(value = "ACTCODE_REGENERATE", description = "重新生成激活码")
|
||||
@PostMapping("/{id}/regenerate")
|
||||
public R regenerate(@PathVariable("id") Long id) {
|
||||
String newCode = dayCardActivationCodeService.regenerateCode(id);
|
||||
Map<String, Object> item = new HashMap<>();
|
||||
item.put("code", newCode);
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("data", item);
|
||||
return R.dataSuccess(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用/禁用激活码
|
||||
*/
|
||||
@ApiOperation("启用/禁用激活码")
|
||||
@AuditAction(value = "ACTCODE_UPDATE_STATUS", description = "修改激活码状态")
|
||||
@PutMapping("/{id}/status")
|
||||
public R updateStatus(@PathVariable("id") Long id, @RequestBody UpdateStatusRequest request) {
|
||||
dayCardActivationCodeService.updateStatus(id, request.getStatus());
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除激活码(软删除)
|
||||
*/
|
||||
@ApiOperation("删除激活码")
|
||||
@AuditAction(value = "ACTCODE_DELETE", description = "删除激活码")
|
||||
@DeleteMapping("/{id}")
|
||||
public R delete(@PathVariable("id") Long id) {
|
||||
dayCardActivationCodeService.delete(id);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
// ==================== 内部方法 ====================
|
||||
|
||||
/**
|
||||
* 从门店 mpath 解析渠道组织ID(mpath 第2段)
|
||||
*/
|
||||
private Long resolveChannelOrgId(SysOrgEntity storeOrg) {
|
||||
if (storeOrg == null || StringUtils.isBlank(storeOrg.getMpath())) {
|
||||
return null;
|
||||
}
|
||||
String[] segments = storeOrg.getMpath().split("\\.");
|
||||
if (segments.length >= 2) {
|
||||
try {
|
||||
return Long.parseLong(segments[1]);
|
||||
} catch (NumberFormatException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Map<String, Object> toMap(DayCardActivationCodeEntity entity) {
|
||||
Map<String, Object> m = new HashMap<>();
|
||||
m.put("id", entity.getId());
|
||||
m.put("code", entity.getCode());
|
||||
m.put("channelOrgId", entity.getChannelOrgId());
|
||||
m.put("storeOrgId", entity.getStoreOrgId());
|
||||
m.put("status", entity.getStatus());
|
||||
m.put("maxDeviceCount", entity.getMaxDeviceCount());
|
||||
m.put("activatedCount", entity.getActivatedCount());
|
||||
m.put("remark", entity.getRemark());
|
||||
m.put("createdAt", entity.getCreatedAt());
|
||||
return m;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成激活码请求
|
||||
*/
|
||||
@lombok.Data
|
||||
public static class GenerateRequest {
|
||||
private Long storeOrgId;
|
||||
private String remark;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新状态请求
|
||||
*/
|
||||
@lombok.Data
|
||||
public static class UpdateStatusRequest {
|
||||
private String status;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
package io.as.modules.daycard.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import io.as.common.annotation.AuditAction;
|
||||
import io.as.common.utils.PageUtils;
|
||||
import io.as.common.utils.R;
|
||||
import io.as.modules.daycard.constants.DayCardConstants;
|
||||
import io.as.modules.daycard.entity.DayCardPoolEntity;
|
||||
import io.as.modules.daycard.service.DayCardOrderService;
|
||||
import io.as.modules.daycard.service.DayCardPoolService;
|
||||
import io.as.modules.daycard.vo.DayCardPoolStatsVo;
|
||||
import org.apache.shiro.authz.annotation.RequiresAuthentication;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 天卡管理后台接口(JWT 鉴权)
|
||||
* <p>供 Web 管理后台调用,与 /openapi/** 合作伙伴接口隔离</p>
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/manage/daycard")
|
||||
@RequiresAuthentication
|
||||
public class DayCardManageController {
|
||||
|
||||
@Autowired
|
||||
private DayCardPoolService dayCardPoolService;
|
||||
|
||||
@Autowired
|
||||
private DayCardOrderService dayCardOrderService;
|
||||
|
||||
// ===================== 天卡池 =====================
|
||||
|
||||
/**
|
||||
* 天卡池列表
|
||||
*/
|
||||
@GetMapping("/pool/list")
|
||||
public R poolList(@RequestParam Map<String, Object> params) {
|
||||
Long partnerId = parsePartnerId(params);
|
||||
PageUtils page = dayCardPoolService.queryPage(params, partnerId);
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("page", page);
|
||||
return R.dataSuccess(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 天卡池统计
|
||||
*/
|
||||
@GetMapping("/pool/stats")
|
||||
public R poolStats(@RequestParam(required = false) Long partnerId) {
|
||||
DayCardPoolStatsVo vo = buildStats(partnerId);
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("stats", vo);
|
||||
return R.dataSuccess(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入天卡卡密
|
||||
*/
|
||||
@AuditAction(value = "DAYCARD_POOL_IMPORT", description = "导入天卡卡密")
|
||||
@PostMapping("/pool/import")
|
||||
public R poolImport(@RequestBody Map<String, Object> body) {
|
||||
Long partnerId = null;
|
||||
Object pid = body.get("partnerId");
|
||||
if (pid != null) {
|
||||
partnerId = Long.parseLong(String.valueOf(pid));
|
||||
}
|
||||
Object codesObj = body.get("cardCodes");
|
||||
if (codesObj == null) {
|
||||
return R.error("cardCodes 不能为空");
|
||||
}
|
||||
@SuppressWarnings("unchecked")
|
||||
List<String> cardCodes = (List<String>) codesObj;
|
||||
if (cardCodes.isEmpty()) {
|
||||
return R.error("cardCodes 不能为空");
|
||||
}
|
||||
if (partnerId == null) {
|
||||
return R.error("partnerId 不能为空");
|
||||
}
|
||||
int success = dayCardPoolService.importCards(cardCodes, partnerId);
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("successCount", success);
|
||||
data.put("totalCount", cardCodes.size());
|
||||
return R.dataSuccess(data);
|
||||
}
|
||||
|
||||
// ===================== 天卡订单 =====================
|
||||
|
||||
/**
|
||||
* 天卡订单列表
|
||||
*/
|
||||
@GetMapping("/order/list")
|
||||
public R orderList(@RequestParam Map<String, Object> params) {
|
||||
Long partnerId = parsePartnerId(params);
|
||||
PageUtils page = dayCardOrderService.queryPage(params, partnerId);
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("page", page);
|
||||
return R.dataSuccess(data);
|
||||
}
|
||||
|
||||
// ===================== 设备权益 =====================
|
||||
|
||||
/**
|
||||
* 查询设备天卡权益
|
||||
*/
|
||||
@GetMapping("/benefits/{deviceId}")
|
||||
public R deviceBenefits(@PathVariable Long deviceId) {
|
||||
DayCardPoolEntity pool = dayCardOrderService.getDeviceBenefits(deviceId);
|
||||
Map<String, Object> item = new HashMap<>();
|
||||
if (pool != null) {
|
||||
item.put("hasBenefits", true);
|
||||
item.put("thirdCardNo", pool.getThirdCardNo());
|
||||
item.put("cardCode", pool.getCardCode());
|
||||
item.put("expireAt", pool.getExpireAt());
|
||||
} else {
|
||||
item.put("hasBenefits", false);
|
||||
}
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("data", item);
|
||||
return R.dataSuccess(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解绑设备天卡权益(Web端手动操作)
|
||||
* <p>将设备绑定的 USED 卡转为 REUSABLE,释放设备。橘视厅权益保留,后续新设备下单可复用此卡。</p>
|
||||
*/
|
||||
@AuditAction(value = "DAYCARD_DEVICE_UNBIND", description = "解绑设备天卡权益")
|
||||
@PostMapping("/device/unbind")
|
||||
public R unbindDevice(@RequestParam Long deviceId) {
|
||||
dayCardOrderService.unbindDevice(deviceId);
|
||||
return R.ok("设备解绑成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 设备组织解绑(更换安装场所时调用)
|
||||
* <p>释放设备当前天卡权益并清除门店组织关联(storeOrgId),设备可在新场所用新激活码重新激活绑定。</p>
|
||||
*/
|
||||
@AuditAction(value = "DAYCARD_DEVICE_UNBIND_ORG", description = "设备组织解绑")
|
||||
@PostMapping("/device/unbind-org")
|
||||
public R unbindDeviceOrg(@RequestParam Long deviceId) {
|
||||
dayCardOrderService.unbindDeviceOrg(deviceId);
|
||||
return R.ok("设备组织解绑成功");
|
||||
}
|
||||
|
||||
// ===================== 内部方法 =====================
|
||||
|
||||
private Long parsePartnerId(Map<String, Object> params) {
|
||||
Object val = params.get("partnerId");
|
||||
if (val == null) {
|
||||
val = params.get("partner_id");
|
||||
}
|
||||
if (val == null || "".equals(val)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return Long.parseLong(String.valueOf(val));
|
||||
} catch (NumberFormatException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private DayCardPoolStatsVo buildStats(Long partnerId) {
|
||||
DayCardPoolStatsVo vo = new DayCardPoolStatsVo();
|
||||
|
||||
int available = dayCardPoolService.getAvailableCount(partnerId);
|
||||
vo.setAvailable(available);
|
||||
|
||||
QueryWrapper<DayCardPoolEntity> reservedWrapper = new QueryWrapper<>();
|
||||
reservedWrapper.eq("delete_flag", DayCardConstants.DELETE_FLAG_UNDEL)
|
||||
.eq("status", DayCardConstants.POOL_STATUS_RESERVED);
|
||||
if (partnerId != null) {
|
||||
reservedWrapper.eq("partner_id", partnerId);
|
||||
}
|
||||
int reserved = dayCardPoolService.count(reservedWrapper);
|
||||
vo.setReserved(reserved);
|
||||
|
||||
QueryWrapper<DayCardPoolEntity> usedWrapper = new QueryWrapper<>();
|
||||
usedWrapper.eq("delete_flag", DayCardConstants.DELETE_FLAG_UNDEL)
|
||||
.eq("status", DayCardConstants.POOL_STATUS_USED);
|
||||
if (partnerId != null) {
|
||||
usedWrapper.eq("partner_id", partnerId);
|
||||
}
|
||||
int used = dayCardPoolService.count(usedWrapper);
|
||||
vo.setUsed(used);
|
||||
|
||||
vo.setTotal(available + reserved + used);
|
||||
return vo;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package io.as.modules.daycard.controller;
|
||||
|
||||
import io.as.common.exception.ASException;
|
||||
import io.as.common.utils.PageUtils;
|
||||
import io.as.common.utils.R;
|
||||
import io.as.modules.daycard.entity.DayCardOrderEntity;
|
||||
import io.as.modules.daycard.service.DayCardOrderService;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 天卡订单 Controller(管理后台用)
|
||||
* <p>App 端接口已迁移至 /api/jst/** (ApiJstController),本 Controller 仅保留管理后台用的分页查询和详情接口</p>
|
||||
*/
|
||||
@Api(tags = "天卡订单")
|
||||
@RestController
|
||||
@RequestMapping("/manage/daycard")
|
||||
public class DayCardOrderController {
|
||||
|
||||
@Autowired
|
||||
private DayCardOrderService dayCardOrderService;
|
||||
|
||||
/**
|
||||
* 分页查询订单
|
||||
*/
|
||||
@ApiOperation("分页查询天卡订单")
|
||||
@GetMapping("/list")
|
||||
public R list(@RequestParam Map<String, Object> params) {
|
||||
Long partnerId = parsePartnerId(params);
|
||||
PageUtils page = dayCardOrderService.queryPage(params, partnerId);
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("page", page);
|
||||
return R.dataSuccess(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询订单详情
|
||||
*/
|
||||
@ApiOperation("查询订单详情")
|
||||
@GetMapping("/info/{orderNo}")
|
||||
public R info(@PathVariable("orderNo") String orderNo) {
|
||||
DayCardOrderEntity order = dayCardOrderService.getByOrderNo(orderNo);
|
||||
if (order == null) {
|
||||
throw new ASException("订单不存在");
|
||||
}
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("order", order);
|
||||
return R.dataSuccess(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从请求参数中解析 partnerId(兼容存量数据查询,新订单 partnerId 为 null)
|
||||
*/
|
||||
private Long parsePartnerId(Map<String, Object> params) {
|
||||
Object val = params.get("partnerId");
|
||||
if (val == null) {
|
||||
val = params.get("partner_id");
|
||||
}
|
||||
if (val == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return Long.parseLong(String.valueOf(val));
|
||||
} catch (NumberFormatException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
package io.as.modules.daycard.controller;
|
||||
|
||||
import io.as.common.annotation.AuditAction;
|
||||
import io.as.common.exception.ASException;
|
||||
import io.as.common.utils.PageUtils;
|
||||
import io.as.common.utils.R;
|
||||
import io.as.modules.daycard.constants.DayCardConstants;
|
||||
import io.as.modules.daycard.entity.DayCardPriceConfigEntity;
|
||||
import io.as.modules.daycard.service.DayCardPriceConfigService;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 天卡定价策略管理 Controller
|
||||
*/
|
||||
@Api(tags = "天卡定价策略管理")
|
||||
@RestController
|
||||
@RequestMapping("/manage/price")
|
||||
public class DayCardPriceConfigController {
|
||||
|
||||
@Autowired
|
||||
private DayCardPriceConfigService dayCardPriceConfigService;
|
||||
|
||||
/**
|
||||
* 分页查询定价策略(支持 partnerId/status 筛选)
|
||||
*/
|
||||
@ApiOperation("分页查询定价策略")
|
||||
@GetMapping("/list")
|
||||
public R list(@RequestParam Map<String, Object> params) {
|
||||
PageUtils page = dayCardPriceConfigService.queryPage(params);
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("page", page);
|
||||
return R.dataSuccess(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询定价策略详情
|
||||
*/
|
||||
@ApiOperation("定价策略详情")
|
||||
@GetMapping("/info/{id}")
|
||||
public R info(@PathVariable("id") Long id) {
|
||||
DayCardPriceConfigEntity config = dayCardPriceConfigService.getById(id);
|
||||
if (config == null) {
|
||||
throw new ASException("定价策略不存在");
|
||||
}
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("config", config);
|
||||
return R.dataSuccess(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询生效中的定价策略(3 层维度)
|
||||
* <p>优先级:门店配置价 > 渠道配置价 > 平台默认价兜底</p>
|
||||
*
|
||||
* @param channelOrgId 渠道组织ID(可空)
|
||||
* @param storeOrgId 门店组织ID(可空)
|
||||
*/
|
||||
@ApiOperation("查询生效中的定价策略")
|
||||
@GetMapping("/effective")
|
||||
public R effective(@RequestParam(value = "channelOrgId", required = false) Long channelOrgId,
|
||||
@RequestParam(value = "storeOrgId", required = false) Long storeOrgId) {
|
||||
DayCardPriceConfigEntity config = dayCardPriceConfigService.getEffectivePrice(channelOrgId, storeOrgId);
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("config", config);
|
||||
return R.dataSuccess(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增定价策略
|
||||
*/
|
||||
@ApiOperation("新增定价策略")
|
||||
@AuditAction(value = "PRICE_SAVE", description = "新增定价策略")
|
||||
@PostMapping("/save")
|
||||
public R save(@RequestBody DayCardPriceConfigEntity config) {
|
||||
Date now = new Date();
|
||||
config.setDeleteFlag(DayCardConstants.DELETE_FLAG_UNDEL);
|
||||
config.setCreatedAt(now);
|
||||
config.setUpdatedAt(now);
|
||||
if (config.getStatus() == null) {
|
||||
config.setStatus("ACTIVE");
|
||||
}
|
||||
dayCardPriceConfigService.save(config);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改定价策略
|
||||
*/
|
||||
@ApiOperation("修改定价策略")
|
||||
@AuditAction(value = "PRICE_UPDATE", description = "修改定价策略")
|
||||
@PostMapping("/update")
|
||||
public R update(@RequestBody DayCardPriceConfigEntity config) {
|
||||
if (config.getId() == null) {
|
||||
throw new ASException("id不能为空");
|
||||
}
|
||||
config.setUpdatedAt(new Date());
|
||||
dayCardPriceConfigService.updateById(config);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* 软删除定价策略
|
||||
*/
|
||||
@ApiOperation("删除定价策略")
|
||||
@AuditAction(value = "PRICE_DELETE", description = "删除定价策略")
|
||||
@PostMapping("/delete")
|
||||
public R delete(@RequestBody Long[] ids) {
|
||||
if (ids == null || ids.length == 0) {
|
||||
throw new ASException("ids不能为空");
|
||||
}
|
||||
List<DayCardPriceConfigEntity> list = dayCardPriceConfigService.listByIds(
|
||||
Arrays.asList(ids));
|
||||
Date now = new Date();
|
||||
for (DayCardPriceConfigEntity config : list) {
|
||||
config.setDeleteFlag(DayCardConstants.DELETE_FLAG_DEL);
|
||||
config.setUpdatedAt(now);
|
||||
}
|
||||
dayCardPriceConfigService.updateBatchById(list);
|
||||
return R.ok();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package io.as.modules.daycard.controller;
|
||||
|
||||
import io.as.common.annotation.AuditAction;
|
||||
import io.as.common.exception.ASException;
|
||||
import io.as.common.utils.PageUtils;
|
||||
import io.as.common.utils.R;
|
||||
import io.as.modules.daycard.entity.DayCardRefundEntity;
|
||||
import io.as.modules.daycard.service.DayCardRefundService;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 天卡退款管理 Controller
|
||||
*/
|
||||
@Api(tags = "天卡退款管理")
|
||||
@RestController
|
||||
@RequestMapping("/manage/refund")
|
||||
public class DayCardRefundController {
|
||||
|
||||
@Autowired
|
||||
private DayCardRefundService dayCardRefundService;
|
||||
|
||||
/**
|
||||
* 分页查询退款记录
|
||||
*/
|
||||
@ApiOperation("分页查询退款记录")
|
||||
@GetMapping("/list")
|
||||
public R list(@RequestParam Map<String, Object> params) {
|
||||
PageUtils page = dayCardRefundService.queryPage(params);
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("page", page);
|
||||
return R.dataSuccess(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 退款详情
|
||||
*/
|
||||
@ApiOperation("退款详情")
|
||||
@GetMapping("/info/{refundNo}")
|
||||
public R info(@PathVariable("refundNo") String refundNo) {
|
||||
DayCardRefundEntity refund = dayCardRefundService.getByRefundNo(refundNo);
|
||||
if (refund == null) {
|
||||
throw new ASException("退款记录不存在");
|
||||
}
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("refund", refund);
|
||||
return R.dataSuccess(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发起退款
|
||||
*
|
||||
* @param params 包含 orderNo(订单号)和 reason(退款原因)
|
||||
*/
|
||||
@ApiOperation("发起退款")
|
||||
@AuditAction(value = "REFUND_APPLY", description = "发起退款")
|
||||
@PostMapping("/apply")
|
||||
public R apply(@RequestBody Map<String, String> params) {
|
||||
String orderNo = params.get("orderNo");
|
||||
if (StringUtils.isBlank(orderNo)) {
|
||||
throw new ASException("orderNo不能为空");
|
||||
}
|
||||
String reason = params.get("reason");
|
||||
DayCardRefundEntity refund = dayCardRefundService.refund(orderNo, reason);
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("refund", refund);
|
||||
return R.dataSuccess(data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
package io.as.modules.daycard.controller;
|
||||
|
||||
import io.as.common.exception.ASException;
|
||||
import io.as.common.utils.PageUtils;
|
||||
import io.as.common.utils.R;
|
||||
import io.as.modules.daycard.entity.DayCardSettleOrderEntity;
|
||||
import io.as.modules.daycard.entity.DayCardSettleRecordEntity;
|
||||
import io.as.modules.daycard.entity.DayCardSettleRuleEntity;
|
||||
import io.as.modules.daycard.service.DayCardSettleOrderService;
|
||||
import io.as.modules.daycard.service.DayCardSettleRecordService;
|
||||
import io.as.modules.daycard.service.DayCardSettleRuleService;
|
||||
import io.as.modules.daycard.service.ProfitCalcService;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 天卡分账管理 Controller
|
||||
*/
|
||||
@Api(tags = "天卡分账管理")
|
||||
@RestController
|
||||
@RequestMapping("/manage/settle")
|
||||
public class DayCardSettleController {
|
||||
|
||||
@Autowired
|
||||
private DayCardSettleRuleService dayCardSettleRuleService;
|
||||
|
||||
@Autowired
|
||||
private DayCardSettleRecordService dayCardSettleRecordService;
|
||||
|
||||
@Autowired
|
||||
private DayCardSettleOrderService dayCardSettleOrderService;
|
||||
|
||||
@Autowired
|
||||
private ProfitCalcService profitCalcService;
|
||||
|
||||
// ==================== 分账规则 ====================
|
||||
|
||||
/**
|
||||
* 分页查询分账规则
|
||||
*/
|
||||
@ApiOperation("分页查询分账规则")
|
||||
@GetMapping("/rule/list")
|
||||
public R ruleList(@RequestParam Map<String, Object> params) {
|
||||
PageUtils page = dayCardSettleRuleService.queryPage(params);
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("page", page);
|
||||
return R.dataSuccess(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 分账规则详情
|
||||
*/
|
||||
@ApiOperation("分账规则详情")
|
||||
@GetMapping("/rule/info/{id}")
|
||||
public R ruleInfo(@PathVariable("id") Long id) {
|
||||
DayCardSettleRuleEntity rule = dayCardSettleRuleService.getById(id);
|
||||
if (rule == null) {
|
||||
throw new ASException("分账规则不存在");
|
||||
}
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("rule", rule);
|
||||
return R.dataSuccess(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增分账规则
|
||||
*/
|
||||
@ApiOperation("新增分账规则")
|
||||
@PostMapping("/rule/save")
|
||||
public R ruleSave(@RequestBody DayCardSettleRuleEntity rule) {
|
||||
if (rule.getName() == null || rule.getName().trim().isEmpty()) {
|
||||
throw new ASException("规则名称不能为空");
|
||||
}
|
||||
if (rule.getShareRatio() == null) {
|
||||
throw new ASException("分账比例不能为空");
|
||||
}
|
||||
dayCardSettleRuleService.saveRule(rule);
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("rule", rule);
|
||||
return R.dataSuccess(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改分账规则
|
||||
*/
|
||||
@ApiOperation("修改分账规则")
|
||||
@PostMapping("/rule/update")
|
||||
public R ruleUpdate(@RequestBody DayCardSettleRuleEntity rule) {
|
||||
if (rule.getId() == null) {
|
||||
throw new ASException("id不能为空");
|
||||
}
|
||||
dayCardSettleRuleService.update(rule);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除分账规则(软删除)
|
||||
*/
|
||||
@ApiOperation("删除分账规则")
|
||||
@PostMapping("/rule/delete")
|
||||
public R ruleDelete(@RequestBody Long[] ids) {
|
||||
dayCardSettleRuleService.delete(ids);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
// ==================== 分账记录 ====================
|
||||
|
||||
/**
|
||||
* 分页查询分账记录
|
||||
*/
|
||||
@ApiOperation("分页查询分账记录")
|
||||
@GetMapping("/record/list")
|
||||
public R recordList(@RequestParam Map<String, Object> params) {
|
||||
PageUtils page = dayCardSettleRecordService.queryPage(params);
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("page", page);
|
||||
return R.dataSuccess(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 分账记录详情
|
||||
*/
|
||||
@ApiOperation("分账记录详情")
|
||||
@GetMapping("/record/info/{id}")
|
||||
public R recordInfo(@PathVariable("id") Long id) {
|
||||
DayCardSettleRecordEntity record = dayCardSettleRecordService.getById(id);
|
||||
if (record == null) {
|
||||
throw new ASException("分账记录不存在");
|
||||
}
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("record", record);
|
||||
return R.dataSuccess(data);
|
||||
}
|
||||
|
||||
// ==================== 分账单 ====================
|
||||
|
||||
/**
|
||||
* 分页查询分账单
|
||||
*/
|
||||
@ApiOperation("分页查询分账单")
|
||||
@GetMapping("/order/list")
|
||||
public R orderList(@RequestParam Map<String, Object> params) {
|
||||
PageUtils page = dayCardSettleOrderService.queryPage(params);
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("page", page);
|
||||
return R.dataSuccess(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 分账单详情
|
||||
*/
|
||||
@ApiOperation("分账单详情")
|
||||
@GetMapping("/order/info/{id}")
|
||||
public R orderInfo(@PathVariable("id") Long id) {
|
||||
DayCardSettleOrderEntity settleOrder = dayCardSettleOrderService.getById(id);
|
||||
if (settleOrder == null) {
|
||||
throw new ASException("分账单不存在");
|
||||
}
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("settleOrder", settleOrder);
|
||||
return R.dataSuccess(data);
|
||||
}
|
||||
|
||||
// ==================== 试算 ====================
|
||||
|
||||
/**
|
||||
* 分润试算
|
||||
*/
|
||||
@ApiOperation("分润试算")
|
||||
@GetMapping("/trial")
|
||||
public R trial(@RequestParam("partnerId") Long partnerId,
|
||||
@RequestParam(value = "channelOrgId", required = false) Long channelOrgId,
|
||||
@RequestParam(value = "storeOrgId", required = false) Long storeOrgId,
|
||||
@RequestParam("totalAmount") BigDecimal totalAmount) {
|
||||
if (partnerId == null) {
|
||||
throw new ASException("partnerId不能为空");
|
||||
}
|
||||
if (totalAmount == null || totalAmount.compareTo(BigDecimal.ZERO) <= 0) {
|
||||
throw new ASException("totalAmount必须大于0");
|
||||
}
|
||||
Map<String, Object> result = profitCalcService.calculate(partnerId, channelOrgId, storeOrgId, totalAmount);
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("data", result);
|
||||
return R.dataSuccess(data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package io.as.modules.daycard.controller;
|
||||
|
||||
import io.as.common.annotation.AuditAction;
|
||||
import io.as.common.exception.ASException;
|
||||
import io.as.common.utils.PageUtils;
|
||||
import io.as.common.utils.R;
|
||||
import io.as.modules.daycard.entity.OrgSettlementAccountEntity;
|
||||
import io.as.modules.daycard.service.OrgSettlementAccountService;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 组织分账接收方 Controller
|
||||
*/
|
||||
@Api(tags = "组织分账接收方管理")
|
||||
@RestController
|
||||
@RequestMapping("/manage/profit-receiver")
|
||||
public class OrgSettlementAccountController {
|
||||
|
||||
@Autowired
|
||||
private OrgSettlementAccountService orgSettlementAccountService;
|
||||
|
||||
/**
|
||||
* 分页查询分账接收方
|
||||
*/
|
||||
@ApiOperation("分页查询分账接收方")
|
||||
@GetMapping("/list")
|
||||
public R list(@RequestParam Map<String, Object> params) {
|
||||
PageUtils page = orgSettlementAccountService.queryPage(params);
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("page", page);
|
||||
return R.dataSuccess(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 分账接收方详情
|
||||
*/
|
||||
@ApiOperation("分账接收方详情")
|
||||
@GetMapping("/info/{id}")
|
||||
public R info(@PathVariable("id") Long id) {
|
||||
OrgSettlementAccountEntity account = orgSettlementAccountService.getById(id);
|
||||
if (account == null) {
|
||||
throw new ASException("分账接收方不存在");
|
||||
}
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("account", account);
|
||||
return R.dataSuccess(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增分账接收方
|
||||
*/
|
||||
@ApiOperation("新增分账接收方")
|
||||
@AuditAction(value = "PROFIT_RECEIVER_SAVE", description = "新增分账接收方")
|
||||
@PostMapping("/save")
|
||||
public R save(@RequestBody OrgSettlementAccountEntity account) {
|
||||
if (account.getOrgId() == null) {
|
||||
throw new ASException("组织ID不能为空");
|
||||
}
|
||||
if (account.getReceiverAccount() == null || account.getReceiverAccount().trim().isEmpty()) {
|
||||
throw new ASException("接收方账号不能为空");
|
||||
}
|
||||
if (account.getReceiverType() == null || account.getReceiverType().trim().isEmpty()) {
|
||||
throw new ASException("接收方类型不能为空");
|
||||
}
|
||||
orgSettlementAccountService.saveAccount(account);
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("account", account);
|
||||
return R.dataSuccess(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除分账接收方(软删除)
|
||||
*/
|
||||
@ApiOperation("删除分账接收方")
|
||||
@AuditAction(value = "PROFIT_RECEIVER_DELETE", description = "删除分账接收方")
|
||||
@PostMapping("/delete")
|
||||
public R delete(@RequestBody Long[] ids) {
|
||||
orgSettlementAccountService.delete(ids);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步接收方到微信
|
||||
*/
|
||||
@ApiOperation("同步接收方到微信")
|
||||
@AuditAction(value = "PROFIT_RECEIVER_SYNC", description = "同步分账接收方到微信")
|
||||
@PostMapping("/sync/{id}")
|
||||
public R sync(@PathVariable("id") Long id) {
|
||||
orgSettlementAccountService.syncToWechat(id);
|
||||
return R.ok();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
package io.as.modules.daycard.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import io.as.common.exception.ASException;
|
||||
import io.as.common.utils.PageUtils;
|
||||
import io.as.common.utils.Query;
|
||||
import io.as.common.utils.R;
|
||||
import io.as.modules.daycard.constants.DayCardConstants;
|
||||
import io.as.modules.daycard.dao.ProfitSharingDetailDao;
|
||||
import io.as.modules.daycard.dao.ProfitSharingOrderDao;
|
||||
import io.as.modules.daycard.entity.ProfitSharingDetailEntity;
|
||||
import io.as.modules.daycard.entity.ProfitSharingOrderEntity;
|
||||
import io.as.modules.sys.dao.SysOrgDao;
|
||||
import io.as.modules.sys.entity.SysOrgEntity;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* 分账记录查询 Controller
|
||||
*/
|
||||
@Api(tags = "分账记录查询")
|
||||
@RestController
|
||||
@RequestMapping("/manage/profit-sharing")
|
||||
public class ProfitSharingQueryController {
|
||||
|
||||
@Autowired
|
||||
private ProfitSharingOrderDao profitSharingOrderDao;
|
||||
|
||||
@Autowired
|
||||
private ProfitSharingDetailDao profitSharingDetailDao;
|
||||
|
||||
@Autowired
|
||||
private SysOrgDao sysOrgDao;
|
||||
|
||||
/**
|
||||
* 分页查询分账单(支持 orderNo/status 筛选),返回详情列表
|
||||
*/
|
||||
@ApiOperation("分页查询分账单")
|
||||
@GetMapping("/list")
|
||||
public R list(@RequestParam Map<String, Object> params) {
|
||||
IPage<ProfitSharingOrderEntity> page = new Query<ProfitSharingOrderEntity>().getPage(params, "created_at", false);
|
||||
|
||||
QueryWrapper<ProfitSharingOrderEntity> wrapper = new QueryWrapper<>();
|
||||
wrapper.eq("delete_flag", DayCardConstants.DELETE_FLAG_UNDEL);
|
||||
|
||||
String orderNo = (String) params.get("orderNo");
|
||||
if (StringUtils.isNotBlank(orderNo)) {
|
||||
wrapper.eq("pay_order_no", orderNo);
|
||||
}
|
||||
String status = (String) params.get("status");
|
||||
if (StringUtils.isNotBlank(status)) {
|
||||
wrapper.eq("status", status);
|
||||
}
|
||||
String outOrderNo = (String) params.get("outOrderNo");
|
||||
if (StringUtils.isNotBlank(outOrderNo)) {
|
||||
wrapper.eq("out_order_no", outOrderNo);
|
||||
}
|
||||
|
||||
IPage<ProfitSharingOrderEntity> result = profitSharingOrderDao.selectPage(page, wrapper);
|
||||
|
||||
// 填充明细列表
|
||||
for (ProfitSharingOrderEntity order : result.getRecords()) {
|
||||
QueryWrapper<ProfitSharingDetailEntity> detailWrapper = new QueryWrapper<>();
|
||||
detailWrapper.eq("sharing_order_id", order.getId());
|
||||
List<ProfitSharingDetailEntity> details = profitSharingDetailDao.selectList(detailWrapper);
|
||||
order.setDetails(details);
|
||||
}
|
||||
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("page", new PageUtils(result));
|
||||
return R.dataSuccess(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 分账单详情 + details 子表
|
||||
*/
|
||||
@ApiOperation("分账单详情")
|
||||
@GetMapping("/info/{id}")
|
||||
public R info(@PathVariable("id") Long id) {
|
||||
ProfitSharingOrderEntity order = profitSharingOrderDao.selectById(id);
|
||||
if (order == null) {
|
||||
throw new ASException("分账单不存在");
|
||||
}
|
||||
QueryWrapper<ProfitSharingDetailEntity> detailWrapper = new QueryWrapper<>();
|
||||
detailWrapper.eq("sharing_order_id", id);
|
||||
List<ProfitSharingDetailEntity> details = profitSharingDetailDao.selectList(detailWrapper);
|
||||
order.setDetails(details);
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("order", order);
|
||||
return R.dataSuccess(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计(参数 startDate, endDate, groupBy)
|
||||
* 返回: { summary: {totalCount, totalAmount, successCount, successAmount}, groups: [{orgId, orgName, orgType, count, amount}] }
|
||||
*/
|
||||
@ApiOperation("分账统计")
|
||||
@GetMapping("/stats")
|
||||
public R stats(@RequestParam(value = "startDate", required = false) String startDate,
|
||||
@RequestParam(value = "endDate", required = false) String endDate,
|
||||
@RequestParam(value = "groupBy", required = false) String groupBy) {
|
||||
QueryWrapper<ProfitSharingOrderEntity> wrapper = new QueryWrapper<>();
|
||||
wrapper.eq("delete_flag", DayCardConstants.DELETE_FLAG_UNDEL);
|
||||
|
||||
if (StringUtils.isNotBlank(startDate)) {
|
||||
wrapper.ge("created_at", startDate + " 00:00:00");
|
||||
}
|
||||
if (StringUtils.isNotBlank(endDate)) {
|
||||
wrapper.le("created_at", endDate + " 23:59:59");
|
||||
}
|
||||
|
||||
List<ProfitSharingOrderEntity> orders = profitSharingOrderDao.selectList(wrapper);
|
||||
|
||||
// 汇总
|
||||
int totalCount = orders.size();
|
||||
int totalAmount = 0;
|
||||
int successCount = 0;
|
||||
int successAmount = 0;
|
||||
for (ProfitSharingOrderEntity order : orders) {
|
||||
if (order.getTotalAmount() != null) {
|
||||
totalAmount += order.getTotalAmount();
|
||||
}
|
||||
if (DayCardConstants.SETTLE_STATUS_SUCCESS.equals(order.getStatus())) {
|
||||
successCount++;
|
||||
if (order.getTotalAmount() != null) {
|
||||
successAmount += order.getTotalAmount();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, Object> summary = new HashMap<>();
|
||||
summary.put("totalCount", totalCount);
|
||||
summary.put("totalAmount", totalAmount);
|
||||
summary.put("successCount", successCount);
|
||||
summary.put("successAmount", successAmount);
|
||||
|
||||
// 分组统计
|
||||
List<Map<String, Object>> groups = new ArrayList<>();
|
||||
if (StringUtils.isNotBlank(groupBy) && "org".equals(groupBy)) {
|
||||
// 按组织分组:查询所有明细,按 orgId 聚合
|
||||
Map<Long, Map<String, Object>> orgMap = new HashMap<>();
|
||||
for (ProfitSharingOrderEntity order : orders) {
|
||||
QueryWrapper<ProfitSharingDetailEntity> detailWrapper = new QueryWrapper<>();
|
||||
detailWrapper.eq("sharing_order_id", order.getId());
|
||||
List<ProfitSharingDetailEntity> details = profitSharingDetailDao.selectList(detailWrapper);
|
||||
for (ProfitSharingDetailEntity detail : details) {
|
||||
Long orgId = detail.getOrgId();
|
||||
if (orgId == null) {
|
||||
continue;
|
||||
}
|
||||
Map<String, Object> group = orgMap.get(orgId);
|
||||
if (group == null) {
|
||||
group = new HashMap<>();
|
||||
group.put("orgId", orgId);
|
||||
group.put("count", 0);
|
||||
group.put("amount", 0);
|
||||
group.put("orgRole", detail.getOrgRole());
|
||||
// 查询组织名称
|
||||
SysOrgEntity org = sysOrgDao.selectById(orgId);
|
||||
if (org != null) {
|
||||
group.put("orgName", org.getName());
|
||||
group.put("orgType", org.getType());
|
||||
}
|
||||
orgMap.put(orgId, group);
|
||||
}
|
||||
group.put("count", (Integer) group.get("count") + 1);
|
||||
if (detail.getAmount() != null) {
|
||||
group.put("amount", (Integer) group.get("amount") + detail.getAmount());
|
||||
}
|
||||
}
|
||||
}
|
||||
groups.addAll(orgMap.values());
|
||||
}
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("summary", summary);
|
||||
result.put("groups", groups);
|
||||
return R.dataSuccess(result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
package io.as.modules.daycard.controller;
|
||||
|
||||
import io.as.common.annotation.AuditAction;
|
||||
import io.as.common.exception.ASException;
|
||||
import io.as.common.utils.PageUtils;
|
||||
import io.as.common.utils.R;
|
||||
import io.as.modules.daycard.constants.DayCardConstants;
|
||||
import io.as.modules.daycard.entity.ProfitSharingRuleEntity;
|
||||
import io.as.modules.daycard.entity.ProfitSharingRuleItemEntity;
|
||||
import io.as.modules.daycard.service.ProfitSharingRuleService;
|
||||
import io.as.modules.sys.entity.SysOrgEntity;
|
||||
import io.as.modules.sys.service.SysOrgService;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 分润规则 Controller
|
||||
*/
|
||||
@Api(tags = "分润规则管理")
|
||||
@RestController
|
||||
@RequestMapping("/manage/profit-rule")
|
||||
public class ProfitSharingRuleController {
|
||||
|
||||
@Autowired
|
||||
private ProfitSharingRuleService profitSharingRuleService;
|
||||
|
||||
@Autowired
|
||||
private SysOrgService sysOrgService;
|
||||
|
||||
/**
|
||||
* 分页查询分润规则
|
||||
*/
|
||||
@ApiOperation("分页查询分润规则")
|
||||
@GetMapping("/list")
|
||||
public R list(@RequestParam Map<String, Object> params) {
|
||||
PageUtils page = profitSharingRuleService.queryPage(params);
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("page", page);
|
||||
return R.dataSuccess(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 分润规则详情(含 items 子表)
|
||||
*/
|
||||
@ApiOperation("分润规则详情")
|
||||
@GetMapping("/info/{id}")
|
||||
public R info(@PathVariable("id") Long id) {
|
||||
ProfitSharingRuleEntity rule = profitSharingRuleService.getInfo(id);
|
||||
if (rule == null) {
|
||||
throw new ASException("分润规则不存在");
|
||||
}
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("rule", rule);
|
||||
return R.dataSuccess(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增分润规则
|
||||
*/
|
||||
@ApiOperation("新增分润规则")
|
||||
@AuditAction(value = "PROFIT_RULE_SAVE", description = "新增分润规则")
|
||||
@PostMapping("/save")
|
||||
public R save(@RequestBody ProfitSharingRuleEntity rule) {
|
||||
if (StringUtils.isBlank(rule.getRuleName())) {
|
||||
throw new ASException("规则名称不能为空");
|
||||
}
|
||||
if (StringUtils.isBlank(rule.getScopeType())) {
|
||||
throw new ASException("范围类型不能为空");
|
||||
}
|
||||
profitSharingRuleService.saveRule(rule);
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("rule", rule);
|
||||
return R.dataSuccess(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改分润规则
|
||||
*/
|
||||
@ApiOperation("修改分润规则")
|
||||
@AuditAction(value = "PROFIT_RULE_UPDATE", description = "修改分润规则")
|
||||
@PostMapping("/update")
|
||||
public R update(@RequestBody ProfitSharingRuleEntity rule) {
|
||||
if (rule.getId() == null) {
|
||||
throw new ASException("id不能为空");
|
||||
}
|
||||
profitSharingRuleService.updateRule(rule);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除分润规则(逻辑删除)
|
||||
*/
|
||||
@ApiOperation("删除分润规则")
|
||||
@AuditAction(value = "PROFIT_RULE_DELETE", description = "删除分润规则")
|
||||
@PostMapping("/delete")
|
||||
public R delete(@RequestBody Map<String, Object> body) {
|
||||
Long id = Long.parseLong(body.get("id").toString());
|
||||
profitSharingRuleService.delete(id);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* 试算:根据门店和金额预览分润结果
|
||||
*/
|
||||
@ApiOperation("分润试算")
|
||||
@AuditAction(value = "PROFIT_RULE_PREVIEW", description = "分润试算")
|
||||
@PostMapping("/preview")
|
||||
public R preview(@RequestBody Map<String, Object> body) {
|
||||
Long storeOrgId = Long.parseLong(body.get("storeOrgId").toString());
|
||||
Long amount = Long.parseLong(body.get("amount").toString());
|
||||
|
||||
// 1. 从 storeOrgId 查 sys_org 获取 mpath,解析出 channelOrgId 和 agentOrgId
|
||||
SysOrgEntity storeOrg = sysOrgService.getById(storeOrgId);
|
||||
if (storeOrg == null) {
|
||||
throw new ASException("门店组织不存在");
|
||||
}
|
||||
String mpath = storeOrg.getMpath();
|
||||
if (StringUtils.isBlank(mpath)) {
|
||||
throw new ASException("门店组织 mpath 为空");
|
||||
}
|
||||
String[] segments = mpath.split("\\.");
|
||||
// segments[0] = PLATFORM, segments[1] = CHANNEL, segments[n-1] = STORE
|
||||
Long platformOrgId = null;
|
||||
Long channelOrgId = null;
|
||||
Long agentOrgId = null;
|
||||
if (segments.length >= 1) {
|
||||
platformOrgId = Long.parseLong(segments[0]);
|
||||
}
|
||||
if (segments.length >= 2) {
|
||||
channelOrgId = Long.parseLong(segments[1]);
|
||||
}
|
||||
// segments.length >= 4 表示路径中有代理商(PLATFORM.CHANNEL.AGENT.STORE)
|
||||
if (segments.length >= 4) {
|
||||
agentOrgId = Long.parseLong(segments[segments.length - 2]);
|
||||
}
|
||||
|
||||
// 2. 调用 matchRule 匹配规则
|
||||
ProfitSharingRuleEntity rule = profitSharingRuleService.matchRule(storeOrgId, agentOrgId, channelOrgId);
|
||||
if (rule == null) {
|
||||
throw new ASException("未匹配到分润规则");
|
||||
}
|
||||
|
||||
// 3. 按 items 计算各方分账金额
|
||||
List<ProfitSharingRuleItemEntity> items = rule.getItems();
|
||||
if (items == null || items.isEmpty()) {
|
||||
throw new ASException("分润规则未配置明细");
|
||||
}
|
||||
|
||||
// 按 sortOrder 排序
|
||||
items.sort((a, b) -> {
|
||||
int sa = a.getSortOrder() == null ? 0 : a.getSortOrder();
|
||||
int sb = b.getSortOrder() == null ? 0 : b.getSortOrder();
|
||||
return Integer.compare(sa, sb);
|
||||
});
|
||||
|
||||
// orgRole -> orgId 映射
|
||||
Map<String, Long> orgIdMap = new HashMap<>();
|
||||
orgIdMap.put(DayCardConstants.ORG_ROLE_PLATFORM, platformOrgId);
|
||||
orgIdMap.put(DayCardConstants.ORG_ROLE_CHANNEL, channelOrgId);
|
||||
orgIdMap.put(DayCardConstants.ORG_ROLE_AGENT, agentOrgId);
|
||||
orgIdMap.put(DayCardConstants.ORG_ROLE_STORE, storeOrgId);
|
||||
|
||||
// 找到最后一个 PLATFORM 项(尾差归平台)
|
||||
int lastPlatformIndex = -1;
|
||||
for (int i = 0; i < items.size(); i++) {
|
||||
if (DayCardConstants.ORG_ROLE_PLATFORM.equals(items.get(i).getOrgRole())) {
|
||||
lastPlatformIndex = i;
|
||||
}
|
||||
}
|
||||
|
||||
// 计算非平台方 RATIO 比例之和
|
||||
BigDecimal nonPlatformTotalRatio = BigDecimal.ZERO;
|
||||
|
||||
// 计算各方分账金额
|
||||
List<Map<String, Object>> details = new ArrayList<>();
|
||||
long allocatedTotal = 0;
|
||||
for (int i = 0; i < items.size(); i++) {
|
||||
ProfitSharingRuleItemEntity item = items.get(i);
|
||||
Map<String, Object> detail = new HashMap<>();
|
||||
detail.put("orgRole", item.getOrgRole());
|
||||
detail.put("shareType", item.getShareType());
|
||||
detail.put("shareValue", item.getShareValue());
|
||||
|
||||
Long orgId = orgIdMap.get(item.getOrgRole());
|
||||
detail.put("orgId", orgId);
|
||||
|
||||
// 查询组织名称
|
||||
String orgName = null;
|
||||
if (orgId != null) {
|
||||
SysOrgEntity org = sysOrgService.getById(orgId);
|
||||
if (org != null) {
|
||||
orgName = org.getName();
|
||||
}
|
||||
}
|
||||
detail.put("orgName", orgName);
|
||||
|
||||
long itemAmount;
|
||||
if (i == lastPlatformIndex) {
|
||||
// 最后一方 PLATFORM 用减法(尾差归平台)
|
||||
itemAmount = amount - allocatedTotal;
|
||||
} else if (DayCardConstants.SHARE_TYPE_FIXED.equals(item.getShareType())) {
|
||||
// FIXED: shareValue 直接作为分
|
||||
itemAmount = item.getShareValue().longValue();
|
||||
allocatedTotal += itemAmount;
|
||||
} else {
|
||||
// RATIO: amount * shareValue / 100
|
||||
BigDecimal ratioAmount = BigDecimal.valueOf(amount)
|
||||
.multiply(item.getShareValue())
|
||||
.divide(BigDecimal.valueOf(100), 0, RoundingMode.DOWN);
|
||||
itemAmount = ratioAmount.longValue();
|
||||
allocatedTotal += itemAmount;
|
||||
}
|
||||
|
||||
// 统计非平台方 RATIO 比例之和
|
||||
if (!DayCardConstants.ORG_ROLE_PLATFORM.equals(item.getOrgRole())
|
||||
&& DayCardConstants.SHARE_TYPE_RATIO.equals(item.getShareType())
|
||||
&& item.getShareValue() != null) {
|
||||
nonPlatformTotalRatio = nonPlatformTotalRatio.add(item.getShareValue());
|
||||
}
|
||||
|
||||
detail.put("amount", itemAmount);
|
||||
details.add(detail);
|
||||
}
|
||||
|
||||
// 5. warning 非 null 时表示超30%
|
||||
String warning = null;
|
||||
if (nonPlatformTotalRatio.compareTo(BigDecimal.valueOf(DayCardConstants.PROFIT_SHARE_LIMIT)) > 0) {
|
||||
warning = "非平台方分润比例之和" + nonPlatformTotalRatio + "%超过上限" + DayCardConstants.PROFIT_SHARE_LIMIT + "%";
|
||||
}
|
||||
|
||||
// 4. 返回结果
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("ruleId", rule.getId());
|
||||
result.put("ruleName", rule.getRuleName());
|
||||
result.put("details", details);
|
||||
result.put("nonPlatformTotalRatio", nonPlatformTotalRatio);
|
||||
result.put("warning", warning);
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("data", result);
|
||||
return R.dataSuccess(data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package io.as.modules.daycard.dao;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import io.as.modules.daycard.entity.DayCardActivationCodeEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 天卡设备激活码 DAO
|
||||
*/
|
||||
@Mapper
|
||||
public interface DayCardActivationCodeDao extends BaseMapper<DayCardActivationCodeEntity> {
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package io.as.modules.daycard.dao;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import io.as.modules.daycard.entity.DayCardImportBatchEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 天卡导入批次
|
||||
*/
|
||||
@Mapper
|
||||
public interface DayCardImportBatchDao extends BaseMapper<DayCardImportBatchEntity> {
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package io.as.modules.daycard.dao;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import io.as.modules.daycard.entity.DayCardOrderEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 天卡订单
|
||||
*/
|
||||
@Mapper
|
||||
public interface DayCardOrderDao extends BaseMapper<DayCardOrderEntity> {
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package io.as.modules.daycard.dao;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import io.as.modules.daycard.entity.DayCardPoolEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
/**
|
||||
* 天卡池
|
||||
*/
|
||||
@Mapper
|
||||
public interface DayCardPoolDao extends BaseMapper<DayCardPoolEntity> {
|
||||
|
||||
/**
|
||||
* 使用 SELECT ... FOR UPDATE SKIP LOCKED 抢占一张未使用的天卡,
|
||||
* 避免多线程/多实例并发下单时分配到同一张卡。
|
||||
* <p>天卡池为通用池,不再按 partner 过滤</p>
|
||||
*
|
||||
* @return 被当前事务锁定的可用天卡行,无可用时返回 null
|
||||
*/
|
||||
DayCardPoolEntity selectOneForUpdateSkipLocked();
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package io.as.modules.daycard.dao;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import io.as.modules.daycard.entity.DayCardPriceConfigEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 天卡定价策略 Dao
|
||||
*/
|
||||
@Mapper
|
||||
public interface DayCardPriceConfigDao extends BaseMapper<DayCardPriceConfigEntity> {
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package io.as.modules.daycard.dao;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import io.as.modules.daycard.entity.DayCardProfitReceiverEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 天卡分账接收方 Dao
|
||||
*/
|
||||
@Mapper
|
||||
public interface DayCardProfitReceiverDao extends BaseMapper<DayCardProfitReceiverEntity> {
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package io.as.modules.daycard.dao;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import io.as.modules.daycard.entity.DayCardProfitRuleEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 天卡分润规则 Dao
|
||||
*/
|
||||
@Mapper
|
||||
public interface DayCardProfitRuleDao extends BaseMapper<DayCardProfitRuleEntity> {
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package io.as.modules.daycard.dao;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import io.as.modules.daycard.entity.DayCardRefundEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 天卡退款单 Dao
|
||||
*/
|
||||
@Mapper
|
||||
public interface DayCardRefundDao extends BaseMapper<DayCardRefundEntity> {
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package io.as.modules.daycard.dao;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import io.as.modules.daycard.entity.DayCardSettleOrderEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 天卡分账单 Dao
|
||||
*/
|
||||
@Mapper
|
||||
public interface DayCardSettleOrderDao extends BaseMapper<DayCardSettleOrderEntity> {
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package io.as.modules.daycard.dao;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import io.as.modules.daycard.entity.DayCardSettleRecordEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 天卡分账记录 Dao
|
||||
*/
|
||||
@Mapper
|
||||
public interface DayCardSettleRecordDao extends BaseMapper<DayCardSettleRecordEntity> {
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package io.as.modules.daycard.dao;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import io.as.modules.daycard.entity.DayCardSettleRuleEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 天卡分账规则 Dao
|
||||
*/
|
||||
@Mapper
|
||||
public interface DayCardSettleRuleDao extends BaseMapper<DayCardSettleRuleEntity> {
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package io.as.modules.daycard.dao;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import io.as.modules.daycard.entity.OrgSettlementAccountEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 组织分账接收方 Dao
|
||||
*/
|
||||
@Mapper
|
||||
public interface OrgSettlementAccountDao extends BaseMapper<OrgSettlementAccountEntity> {
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package io.as.modules.daycard.dao;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import io.as.modules.daycard.entity.ProfitSharingDetailEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 分账明细 Dao
|
||||
*/
|
||||
@Mapper
|
||||
public interface ProfitSharingDetailDao extends BaseMapper<ProfitSharingDetailEntity> {
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package io.as.modules.daycard.dao;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import io.as.modules.daycard.entity.ProfitSharingOrderEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 分账单 Dao
|
||||
*/
|
||||
@Mapper
|
||||
public interface ProfitSharingOrderDao extends BaseMapper<ProfitSharingOrderEntity> {
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package io.as.modules.daycard.dao;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import io.as.modules.daycard.entity.ProfitSharingRuleEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 分润规则 Dao
|
||||
*/
|
||||
@Mapper
|
||||
public interface ProfitSharingRuleDao extends BaseMapper<ProfitSharingRuleEntity> {
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package io.as.modules.daycard.dao;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import io.as.modules.daycard.entity.ProfitSharingRuleItemEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 分润规则明细 Dao
|
||||
*/
|
||||
@Mapper
|
||||
public interface ProfitSharingRuleItemDao extends BaseMapper<ProfitSharingRuleItemEntity> {
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package io.as.modules.daycard.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 天卡设备激活码
|
||||
* <p>管理后台按门店生成,App 端 init 时输入激活码完成设备注册绑定</p>
|
||||
*/
|
||||
@Data
|
||||
@TableName("day_card_activation_code")
|
||||
public class DayCardActivationCodeEntity implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/** 激活码(全局唯一) */
|
||||
private String code;
|
||||
|
||||
/** 渠道组织ID */
|
||||
private Long channelOrgId;
|
||||
|
||||
/** 门店组织ID */
|
||||
private Long storeOrgId;
|
||||
|
||||
/** 状态:ACTIVE/DISABLED */
|
||||
private String status;
|
||||
|
||||
/** 最大可激活设备数(0=不限) */
|
||||
private Integer maxDeviceCount;
|
||||
|
||||
/** 已激活设备数 */
|
||||
private Integer activatedCount;
|
||||
|
||||
/** 备注 */
|
||||
private String remark;
|
||||
|
||||
/** 删除标记:UNDEL/DEL */
|
||||
private String deleteFlag;
|
||||
|
||||
/** 创建时间 */
|
||||
private Date createdAt;
|
||||
|
||||
/** 更新时间 */
|
||||
private Date updatedAt;
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package io.as.modules.daycard.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 天卡导入批次
|
||||
*/
|
||||
@Data
|
||||
@TableName("day_card_import_batch")
|
||||
public class DayCardImportBatchEntity implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
/** 批次号 */
|
||||
private String batchNo;
|
||||
/** 合作商ID */
|
||||
private Long partnerId;
|
||||
/** 状态:PROCESSING/SUCCEEDED/PARTIAL_FAILED/FAILED */
|
||||
private String status;
|
||||
/** 总数 */
|
||||
private Integer totalCount;
|
||||
/** 成功数 */
|
||||
private Integer successCount;
|
||||
/** 失败数 */
|
||||
private Integer failCount;
|
||||
/** 结果报告URL */
|
||||
private String resultReportUrl;
|
||||
/** 备注 */
|
||||
private String remark;
|
||||
/** 创建人 */
|
||||
private Long createdBy;
|
||||
/** 删除标记 */
|
||||
private String deleteFlag;
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date createdAt;
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date updatedAt;
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package io.as.modules.daycard.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 天卡订单
|
||||
*/
|
||||
@Data
|
||||
@TableName("day_card_order")
|
||||
public class DayCardOrderEntity implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
/** 订单号 */
|
||||
private String orderNo;
|
||||
/** 合作商ID */
|
||||
private Long partnerId;
|
||||
/** 设备ID */
|
||||
private Long deviceId;
|
||||
/** 门店组织ID */
|
||||
private Long storeOrgId;
|
||||
/** 渠道组织ID */
|
||||
private Long channelOrgId;
|
||||
/** 支付后写入的卡号 */
|
||||
private String cardNo;
|
||||
/** 订单状态:CREATED/PAYING/PAID/PAY_FAILED/CANCELLED */
|
||||
private String status;
|
||||
/** 订单金额 */
|
||||
private BigDecimal amount;
|
||||
/** 支付单号 */
|
||||
private String payNo;
|
||||
/** 支付金额 */
|
||||
private BigDecimal payAmount;
|
||||
/** 支付时间 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date payTime;
|
||||
/** 订单过期时间 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date expireTime;
|
||||
/** 取消时间 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date cancelTime;
|
||||
/** 删除标记 */
|
||||
private String deleteFlag;
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date createdAt;
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date updatedAt;
|
||||
/** 支付渠道 MOCK/WECHAT */
|
||||
private String payChannel;
|
||||
/** 微信预支付ID */
|
||||
private String prepayId;
|
||||
/** 微信扫码支付链接 */
|
||||
private String codeUrl;
|
||||
/** 设备SN(冗余,便于排查) */
|
||||
private String deviceSn;
|
||||
/** 设备MAC(冗余) */
|
||||
private String mac;
|
||||
/** 代理商组织ID */
|
||||
private Long agentOrgId;
|
||||
/** 组织链路快照(如 1.5.12.) */
|
||||
private String orgPathSnapshot;
|
||||
/** 微信支付订单号 */
|
||||
private String channelOrderNo;
|
||||
|
||||
/** 合作商名称(非表字段) */
|
||||
@TableField(exist = false)
|
||||
private String partnerName;
|
||||
/** 设备MAC地址(非表字段) */
|
||||
@TableField(exist = false)
|
||||
private String deviceMac;
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package io.as.modules.daycard.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 天卡池
|
||||
*/
|
||||
@Data
|
||||
@TableName("day_card_pool")
|
||||
public class DayCardPoolEntity implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
/** 卡密 */
|
||||
private String cardCode;
|
||||
/** 第三方引用标识 */
|
||||
private String thirdPartyRef;
|
||||
/** 第三方卡号 */
|
||||
private String thirdCardNo;
|
||||
/** 卡类型,默认DAY */
|
||||
private String cardType;
|
||||
/** 合作商ID */
|
||||
private Long partnerId;
|
||||
/** 状态:UNUSED/RESERVED/USED */
|
||||
private String status;
|
||||
/** 激活状态:PENDING/SUCCESS/FAILED */
|
||||
private String activateStatus;
|
||||
/** 激活失败原因 */
|
||||
private String activateError;
|
||||
/** 激活成功时间 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date activatedAt;
|
||||
/** 关联订单号 */
|
||||
private String orderNo;
|
||||
/** 设备ID */
|
||||
private Long deviceId;
|
||||
/** 导入批次ID */
|
||||
private Long importBatchId;
|
||||
/** 卡过期时间 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date expireAt;
|
||||
/** 使用时间 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date usedAt;
|
||||
/** 删除标记 */
|
||||
private String deleteFlag;
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date createdAt;
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date updatedAt;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package io.as.modules.daycard.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 天卡定价策略
|
||||
*/
|
||||
@Data
|
||||
@TableName("day_card_price_config")
|
||||
public class DayCardPriceConfigEntity implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
/** 合作伙伴ID */
|
||||
private Long partnerId;
|
||||
/** 渠道组织ID(可空=全渠道默认) */
|
||||
private Long channelOrgId;
|
||||
/** 门店组织ID(空=渠道级或平台级) */
|
||||
private Long storeOrgId;
|
||||
/** 天卡价格(元) */
|
||||
private BigDecimal price;
|
||||
/** 策略名称 */
|
||||
private String name;
|
||||
/** 状态 ACTIVE/DISABLED */
|
||||
private String status;
|
||||
/** 删除标记 */
|
||||
private String deleteFlag;
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date createdAt;
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date updatedAt;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package io.as.modules.daycard.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 天卡分账接收方
|
||||
*/
|
||||
@Data
|
||||
@TableName("day_card_profit_receiver")
|
||||
public class DayCardProfitReceiverEntity implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
/** 名称 */
|
||||
private String name;
|
||||
/** 合作伙伴ID */
|
||||
private Long partnerId;
|
||||
/** 接收方类型 MERCHANT_ID/PERSONAL_OPENID */
|
||||
private String receiverType;
|
||||
/** 分账接收方账户 */
|
||||
private String receiverAccount;
|
||||
/** 接收方名称 */
|
||||
private String receiverName;
|
||||
/** 微信绑定状态 BINDED/UNBOUND */
|
||||
private String wxBindStatus;
|
||||
/** 备注 */
|
||||
private String remark;
|
||||
/** 删除标记 */
|
||||
private String deleteFlag;
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date createdAt;
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date updatedAt;
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package io.as.modules.daycard.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 天卡分润规则
|
||||
*/
|
||||
@Data
|
||||
@TableName("day_card_profit_rule")
|
||||
public class DayCardProfitRuleEntity implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
/** 规则名称 */
|
||||
private String name;
|
||||
/** 合作伙伴ID */
|
||||
private Long partnerId;
|
||||
/** 渠道组织ID */
|
||||
private Long channelOrgId;
|
||||
/** 门店组织ID(空=渠道级或合作商级) */
|
||||
private Long storeOrgId;
|
||||
/** 接收方类型 MERCHANT_ID/PERSONAL_OPENID */
|
||||
private String receiverType;
|
||||
/** 分账接收方账户 */
|
||||
private String receiverAccount;
|
||||
/** 接收方名称 */
|
||||
private String receiverName;
|
||||
/** 分润比例(如0.3000=30%)或固定金额 */
|
||||
private BigDecimal shareRatio;
|
||||
/** 分润类型 RATIO/FIXED */
|
||||
private String shareType;
|
||||
/** 优先级(数字越大优先级越高) */
|
||||
private Integer priority;
|
||||
/** 状态 ACTIVE/DISABLED */
|
||||
private String status;
|
||||
/** 备注 */
|
||||
private String remark;
|
||||
/** 删除标记 */
|
||||
private String deleteFlag;
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date createdAt;
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date updatedAt;
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package io.as.modules.daycard.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 天卡退款单
|
||||
*/
|
||||
@Data
|
||||
@TableName("day_card_refund")
|
||||
public class DayCardRefundEntity implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
/** 退款单号 */
|
||||
private String refundNo;
|
||||
/** 关联订单号 */
|
||||
private String orderNo;
|
||||
/** 微信退款单号 */
|
||||
private String wxRefundId;
|
||||
/** 退款金额 */
|
||||
private BigDecimal refundAmount;
|
||||
/** 退款原因 */
|
||||
private String reason;
|
||||
/** 状态 PENDING/PROCESSING/SUCCESS/FAILED/CLOSED */
|
||||
private String status;
|
||||
/** 错误信息 */
|
||||
private String errorMessage;
|
||||
/** 退款完成时间 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date settledAt;
|
||||
/** 删除标记 */
|
||||
private String deleteFlag;
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date createdAt;
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date updatedAt;
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package io.as.modules.daycard.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 天卡分账单
|
||||
*/
|
||||
@Data
|
||||
@TableName("day_card_settle_order")
|
||||
public class DayCardSettleOrderEntity implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
/** 分账单号 */
|
||||
private String settleNo;
|
||||
/** 订单号 */
|
||||
private String orderNo;
|
||||
/** 合作伙伴ID */
|
||||
private Long partnerId;
|
||||
/** 分润规则ID */
|
||||
private Long ruleId;
|
||||
/** 接收方账户 */
|
||||
private String receiverAccount;
|
||||
/** 接收方名称 */
|
||||
private String receiverName;
|
||||
/** 分润比例/金额 */
|
||||
private BigDecimal shareRatio;
|
||||
/** 分润类型 RATIO/FIXED */
|
||||
private String shareType;
|
||||
/** 分账金额 */
|
||||
private BigDecimal settleAmount;
|
||||
/** 订单总金额 */
|
||||
private BigDecimal totalAmount;
|
||||
/** 状态 PENDING/PROCESSING/SUCCESS/FAILED */
|
||||
private String status;
|
||||
/** 错误信息 */
|
||||
private String errorMessage;
|
||||
/** 分账完成时间 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date settledAt;
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date createdAt;
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date updatedAt;
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package io.as.modules.daycard.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 天卡分账记录
|
||||
*/
|
||||
@Data
|
||||
@TableName("day_card_settle_record")
|
||||
public class DayCardSettleRecordEntity implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
/** 订单号 */
|
||||
private String orderNo;
|
||||
/** 分账规则ID */
|
||||
private Long settleRuleId;
|
||||
/** 订单总金额 */
|
||||
private BigDecimal totalAmount;
|
||||
/** 分账比例 */
|
||||
private BigDecimal shareRatio;
|
||||
/** 分账金额 */
|
||||
private BigDecimal settleAmount;
|
||||
/** 接收方账户 */
|
||||
private String receiverAccount;
|
||||
/** 状态 PENDING/SUCCESS/FAILED */
|
||||
private String status;
|
||||
/** 错误信息 */
|
||||
private String errorMessage;
|
||||
/** 分账完成时间 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date settledAt;
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date createdAt;
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date updatedAt;
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package io.as.modules.daycard.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 天卡分账规则
|
||||
*/
|
||||
@Data
|
||||
@TableName("day_card_settle_rule")
|
||||
public class DayCardSettleRuleEntity implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
/** 规则名称 */
|
||||
private String name;
|
||||
/** 合作伙伴ID */
|
||||
private Long partnerId;
|
||||
/** 渠道组织ID */
|
||||
private Long channelOrgId;
|
||||
/** 门店组织ID(空=渠道级) */
|
||||
private Long storeOrgId;
|
||||
/** 分账接收方账户 */
|
||||
private String receiverAccount;
|
||||
/** 接收方名称 */
|
||||
private String receiverName;
|
||||
/** 分账比例(如0.3000=30%) */
|
||||
private BigDecimal shareRatio;
|
||||
/** 接收方类型 MERCHANT_ID/PERSONAL_OPENID */
|
||||
private String receiverType;
|
||||
/** 状态 ACTIVE/DISABLED */
|
||||
private String status;
|
||||
/** 删除标记 */
|
||||
private String deleteFlag;
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date createdAt;
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date updatedAt;
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package io.as.modules.daycard.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 组织分账接收方
|
||||
*/
|
||||
@Data
|
||||
@TableName("org_settlement_account")
|
||||
public class OrgSettlementAccountEntity implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
/** 关联组织 */
|
||||
private Long orgId;
|
||||
/** 支付渠道 */
|
||||
private String payChannel;
|
||||
/** 接收方类型 MERCHANT_ID / PERSONAL_OPENID */
|
||||
private String receiverType;
|
||||
/** 接收方账号(商户号或OpenID) */
|
||||
private String receiverAccount;
|
||||
/** 接收方名称 */
|
||||
private String receiverName;
|
||||
/** 绑定状态 PENDING/ACTIVE/FAILED */
|
||||
private String bindStatus;
|
||||
/** 微信侧绑定ID */
|
||||
private String externalBindId;
|
||||
/** 删除标记 */
|
||||
private String deleteFlag;
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date createdAt;
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date updatedAt;
|
||||
|
||||
/** 组织名称(非表字段) */
|
||||
@TableField(exist = false)
|
||||
private String orgName;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package io.as.modules.daycard.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 分账明细
|
||||
*/
|
||||
@Data
|
||||
@TableName("profit_sharing_detail")
|
||||
public class ProfitSharingDetailEntity implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
/** 关联分账单 */
|
||||
private Long sharingOrderId;
|
||||
/** 接收方组织ID */
|
||||
private Long orgId;
|
||||
/** 组织角色 PLATFORM/CHANNEL/AGENT/STORE */
|
||||
private String orgRole;
|
||||
/** 接收方账号 */
|
||||
private String receiverAccount;
|
||||
/** 接收方类型 */
|
||||
private String receiverType;
|
||||
/** 分账金额(分) */
|
||||
private Integer amount;
|
||||
/** 分账描述 */
|
||||
private String description;
|
||||
/** 状态 PENDING/SUCCESS/FAILED */
|
||||
private String status;
|
||||
/** 微信侧分账单号 */
|
||||
private String externalOrderId;
|
||||
/** 成功时间 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date successTime;
|
||||
/** 失败原因 */
|
||||
private String failReason;
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date createdAt;
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date updatedAt;
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package io.as.modules.daycard.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 分账单
|
||||
*/
|
||||
@Data
|
||||
@TableName("profit_sharing_order")
|
||||
public class ProfitSharingOrderEntity implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
/** 关联支付订单号 */
|
||||
private String payOrderNo;
|
||||
/** 微信支付订单号 */
|
||||
private String transactionId;
|
||||
/** 分账方内部单号 */
|
||||
private String outOrderNo;
|
||||
/** 状态 PENDING/SUCCESS/FAILED/PARTIAL */
|
||||
private String status;
|
||||
/** 分账总金额(分) */
|
||||
private Integer totalAmount;
|
||||
/** 命中的规则ID */
|
||||
private Long ruleId;
|
||||
/** 规则快照(JSON) */
|
||||
private String ruleSnapshot;
|
||||
/** 组织链路快照 */
|
||||
private String orgPathSnapshot;
|
||||
/** 分账渠道 */
|
||||
private String sharingChannel;
|
||||
/** 删除标记 */
|
||||
private String deleteFlag;
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date createdAt;
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date updatedAt;
|
||||
|
||||
/** 分账明细列表(非表字段) */
|
||||
@TableField(exist = false)
|
||||
private List<ProfitSharingDetailEntity> details;
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package io.as.modules.daycard.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 分润规则(主表)
|
||||
*/
|
||||
@Data
|
||||
@TableName("profit_sharing_rule")
|
||||
public class ProfitSharingRuleEntity implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/** 规则名称 */
|
||||
private String ruleName;
|
||||
|
||||
/** 范围类型 GLOBAL/CHANNEL/AGENT/STORE */
|
||||
private String scopeType;
|
||||
|
||||
/** 关联组织ID(GLOBAL时为空) */
|
||||
private Long scopeOrgId;
|
||||
|
||||
/** 优先级 */
|
||||
private Integer priority;
|
||||
|
||||
/** 状态 ACTIVE/DISABLED */
|
||||
private String status;
|
||||
|
||||
/** 生效开始时间 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date effectiveFrom;
|
||||
|
||||
/** 生效结束时间 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date effectiveTo;
|
||||
|
||||
/** 删除标记 UNDEL/DEL */
|
||||
private String deleteFlag;
|
||||
|
||||
/** 创建时间 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date createdAt;
|
||||
|
||||
/** 更新时间 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date updatedAt;
|
||||
|
||||
/** 子表:分润规则明细(非表字段) */
|
||||
@TableField(exist = false)
|
||||
private List<ProfitSharingRuleItemEntity> items;
|
||||
|
||||
/** 关联组织名称(非表字段) */
|
||||
@TableField(exist = false)
|
||||
private String scopeOrgName;
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package io.as.modules.daycard.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 分润规则明细(子表)
|
||||
*/
|
||||
@Data
|
||||
@TableName("profit_sharing_rule_item")
|
||||
public class ProfitSharingRuleItemEntity implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/** 关联规则ID */
|
||||
private Long ruleId;
|
||||
|
||||
/** 组织角色 PLATFORM/CHANNEL/AGENT/STORE */
|
||||
private String orgRole;
|
||||
|
||||
/** 分润类型 RATIO/FIXED */
|
||||
private String shareType;
|
||||
|
||||
/** 分润值(比例:百分比;固定:分) */
|
||||
private BigDecimal shareValue;
|
||||
|
||||
/** 排序 */
|
||||
private Integer sortOrder;
|
||||
|
||||
/** 创建时间 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date createdAt;
|
||||
|
||||
/** 更新时间 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date updatedAt;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package io.as.modules.daycard.form;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 天卡订单创建请求
|
||||
*/
|
||||
@Data
|
||||
public class DayCardOrderCreateRequest implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 设备MAC(优先) */
|
||||
private String mac;
|
||||
/** 设备SN(次优) */
|
||||
private String deviceSn;
|
||||
/** 设备UID(兜底) */
|
||||
private String deviceUid;
|
||||
/** 媒资ID */
|
||||
private String mediaId;
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package io.as.modules.daycard.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import io.as.modules.daycard.entity.DayCardActivationCodeEntity;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 天卡设备激活码服务
|
||||
*/
|
||||
public interface DayCardActivationCodeService extends IService<DayCardActivationCodeEntity> {
|
||||
|
||||
/**
|
||||
* 根据激活码查询有效记录(未删除、状态为 ACTIVE)
|
||||
*
|
||||
* @param code 激活码字符串
|
||||
* @return 激活码实体,不存在或无效返回 null
|
||||
*/
|
||||
DayCardActivationCodeEntity getByCode(String code);
|
||||
|
||||
/**
|
||||
* 按门店查询激活码列表
|
||||
*
|
||||
* @param storeOrgId 门店组织ID
|
||||
* @return 激活码列表
|
||||
*/
|
||||
List<DayCardActivationCodeEntity> listByStoreOrgId(Long storeOrgId);
|
||||
|
||||
/**
|
||||
* 为门店生成激活码(每门店一个,已存在则返回已有的)
|
||||
*
|
||||
* @param storeOrgId 门店组织ID
|
||||
* @param channelOrgId 渠道组织ID
|
||||
* @param remark 备注(可选)
|
||||
* @return 激活码实体
|
||||
*/
|
||||
DayCardActivationCodeEntity generateForStore(Long storeOrgId, Long channelOrgId, String remark);
|
||||
|
||||
/**
|
||||
* 重新生成激活码 code(旧 code 失效)
|
||||
*
|
||||
* @param id 激活码记录ID
|
||||
* @return 新激活码字符串
|
||||
*/
|
||||
String regenerateCode(Long id);
|
||||
|
||||
/**
|
||||
* 启用/禁用激活码
|
||||
*
|
||||
* @param id 激活码记录ID
|
||||
* @param status 目标状态:ACTIVE/DISABLED
|
||||
*/
|
||||
void updateStatus(Long id, String status);
|
||||
|
||||
/**
|
||||
* 删除激活码(软删除)
|
||||
*
|
||||
* @param id 激活码记录ID
|
||||
*/
|
||||
void delete(Long id);
|
||||
|
||||
/**
|
||||
* 增加已激活设备计数
|
||||
*
|
||||
* @param id 激活码记录ID
|
||||
*/
|
||||
void incrementActivatedCount(Long id);
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
package io.as.modules.daycard.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import io.as.common.utils.PageUtils;
|
||||
import io.as.modules.daycard.entity.DayCardOrderEntity;
|
||||
import io.as.modules.daycard.entity.DayCardPoolEntity;
|
||||
import io.as.modules.daycard.entity.DayCardPriceConfigEntity;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 天卡订单 Service
|
||||
*/
|
||||
public interface DayCardOrderService extends IService<DayCardOrderEntity> {
|
||||
|
||||
/**
|
||||
* 分页查询天卡订单
|
||||
*
|
||||
* @param params 查询参数
|
||||
* @param partnerId 合作商ID
|
||||
*/
|
||||
PageUtils queryPage(Map<String, Object> params, Long partnerId);
|
||||
|
||||
/**
|
||||
* 创建天卡订单(从 sign 鉴权上下文获取 deviceId)
|
||||
* <p>流程:Redis防重 -> 解析设备组织链路 -> 按渠道定价 -> 生成订单号 -> 插入订单 -> SKIP LOCKED抢卡 -> 乐观锁更新卡状态 -> 返回订单</p>
|
||||
*
|
||||
* @param deviceId 设备ID(从 sign 解析)
|
||||
*/
|
||||
DayCardOrderEntity createOrder(Long deviceId);
|
||||
|
||||
/**
|
||||
* 创建天卡订单(通过设备标识,扫码后调用)
|
||||
* <p>流程:Redis防重(mac/deviceSn/deviceUid)-> 解析设备组织链路 -> 服务端定价 -> 生成订单号 -> 插入订单 -> SKIP LOCKED抢卡 -> 乐观锁更新卡状态 -> 返回订单</p>
|
||||
*
|
||||
* @param mac 设备MAC(优先)
|
||||
* @param deviceSn 设备SN(次优)
|
||||
* @param deviceUid 设备UID(兜底)
|
||||
* @param partnerId 合作商ID
|
||||
*/
|
||||
DayCardOrderEntity createOrder(String mac, String deviceSn, String deviceUid, Long partnerId);
|
||||
|
||||
/**
|
||||
* 支付回调
|
||||
* <p>流程:幂等检查 -> 乐观锁更新订单PAID -> 更新卡状态USED -> 异步激活 -> 清理Redis</p>
|
||||
*
|
||||
* @param orderNo 订单号
|
||||
* @param payNo 支付单号
|
||||
* @param payAmount 支付金额
|
||||
*/
|
||||
void payCallback(String orderNo, String payNo, BigDecimal payAmount);
|
||||
|
||||
/**
|
||||
* 取消超时未支付的订单
|
||||
* <p>查询超时订单(状态CREATED, expire_time < now, LIMIT 200)→ 逐个取消(释放卡+更新订单CANCELLED)</p>
|
||||
*
|
||||
* @return 取消的订单数量
|
||||
*/
|
||||
int cancelTimeoutOrders();
|
||||
|
||||
/**
|
||||
* 根据订单号查询订单
|
||||
*
|
||||
* @param orderNo 订单号
|
||||
*/
|
||||
DayCardOrderEntity getByOrderNo(String orderNo);
|
||||
|
||||
/**
|
||||
* 查询设备的天卡权益(激活后 App 获取播放凭证)
|
||||
* <p>从 day_card_pool 表查询 status=USED 且 expire_at > NOW() 的卡,返回 thirdCardNo 和 expireAt</p>
|
||||
*
|
||||
* @param deviceId 设备ID
|
||||
* @return 天卡池实体,无有效权益时返回 null
|
||||
*/
|
||||
DayCardPoolEntity getDeviceBenefits(Long deviceId);
|
||||
|
||||
/**
|
||||
* 查询设备生效的天卡定价(供前端二维码展示)
|
||||
* <p>按 3 层维度匹配:门店 > 渠道 > 平台默认</p>
|
||||
*
|
||||
* @param deviceId 设备ID
|
||||
* @return 定价策略,无匹配时返回 null
|
||||
*/
|
||||
DayCardPriceConfigEntity getDevicePrice(Long deviceId);
|
||||
|
||||
/**
|
||||
* 解绑设备天卡权益(Web端手动操作)
|
||||
* <p>将 USED 卡转为 REUSABLE,释放设备绑定,保留橘视厅权益供后续设备复用</p>
|
||||
*
|
||||
* @param deviceId 设备ID
|
||||
*/
|
||||
void unbindDevice(Long deviceId);
|
||||
|
||||
/**
|
||||
* 设备组织解绑(更换安装场所时使用)
|
||||
* <p>释放设备当前天卡权益并清除门店组织关联(storeOrgId),
|
||||
* 设备可在新场所用新激活码重新激活绑定。</p>
|
||||
*
|
||||
* @param deviceId 设备ID
|
||||
*/
|
||||
void unbindDeviceOrg(Long deviceId);
|
||||
|
||||
/**
|
||||
* 异步激活天卡:调用橘视厅接口
|
||||
* <p>通过 Spring AOP 代理调用以确保 @Async 生效</p>
|
||||
*
|
||||
* @param order 天卡订单
|
||||
*/
|
||||
void asyncActivateCard(DayCardOrderEntity order);
|
||||
|
||||
/**
|
||||
* 重试激活失败的天卡
|
||||
* <p>补偿定时任务调用:查询 activate_status=FAILED 的卡,重新激活</p>
|
||||
*
|
||||
* @return 重试的卡数量
|
||||
*/
|
||||
int retryFailedActivations();
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package io.as.modules.daycard.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import io.as.common.utils.PageUtils;
|
||||
import io.as.modules.daycard.entity.DayCardPoolEntity;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 天卡池 Service
|
||||
*/
|
||||
public interface DayCardPoolService extends IService<DayCardPoolEntity> {
|
||||
|
||||
/**
|
||||
* 分页查询天卡池
|
||||
*
|
||||
* @param params 查询参数
|
||||
* @param partnerId 合作商ID
|
||||
*/
|
||||
PageUtils queryPage(Map<String, Object> params, Long partnerId);
|
||||
|
||||
/**
|
||||
* 获取可用天卡数量
|
||||
*
|
||||
* @param partnerId 合作商ID
|
||||
*/
|
||||
int getAvailableCount(Long partnerId);
|
||||
|
||||
/**
|
||||
* 批量导入天卡卡密
|
||||
* <p>只需提供 cardCode(橘视厅激活码),thirdPartyRef 和 thirdCardNo 由服务端自动生成</p>
|
||||
*
|
||||
* @param cardCodes 激活码列表(从橘视厅/供应商获取的真实卡密)
|
||||
* @param partnerId 合作商ID
|
||||
* @return 成功导入数量
|
||||
*/
|
||||
int importCards(java.util.List<String> cardCodes, Long partnerId);
|
||||
|
||||
/**
|
||||
* 生成全局唯一的第三方引用号(uid):yyyyMMdd-HHmmss-xxxx
|
||||
*/
|
||||
String generateUniqueThirdPartyRef();
|
||||
|
||||
/**
|
||||
* 生成 24 位随机第三方卡号(0-9 A-Z a-z)
|
||||
*/
|
||||
String generateThirdCardNo();
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package io.as.modules.daycard.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import io.as.common.utils.PageUtils;
|
||||
import io.as.modules.daycard.entity.DayCardPriceConfigEntity;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 天卡定价策略 Service
|
||||
*/
|
||||
public interface DayCardPriceConfigService extends IService<DayCardPriceConfigEntity> {
|
||||
|
||||
/**
|
||||
* 分页查询定价策略
|
||||
*/
|
||||
PageUtils queryPage(Map<String, Object> params);
|
||||
|
||||
/**
|
||||
* 获取有效定价策略(3 层维度,优先级从高到低)
|
||||
* <p>1. 门店配置价(storeOrgId 命中)</p>
|
||||
* <p>2. 渠道配置价(storeOrgId 为空,channelOrgId 命中)</p>
|
||||
* <p>3. 平台默认价兜底(storeOrgId、channelOrgId 均为空)</p>
|
||||
*
|
||||
* @param channelOrgId 渠道组织ID(可空)
|
||||
* @param storeOrgId 门店组织ID(可空)
|
||||
* @return 定价策略,无匹配时返回 null
|
||||
*/
|
||||
DayCardPriceConfigEntity getEffectivePrice(Long channelOrgId, Long storeOrgId);
|
||||
|
||||
/**
|
||||
* 根据组织链路获取价格金额
|
||||
*
|
||||
* @param channelOrgId 渠道组织ID(可空)
|
||||
* @param storeOrgId 门店组织ID(可空)
|
||||
* @return 价格金额,无匹配时返回 null
|
||||
*/
|
||||
BigDecimal getPrice(Long channelOrgId, Long storeOrgId);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package io.as.modules.daycard.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import io.as.common.utils.PageUtils;
|
||||
import io.as.modules.daycard.entity.DayCardProfitReceiverEntity;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 天卡分账接收方 Service
|
||||
*/
|
||||
public interface DayCardProfitReceiverService extends IService<DayCardProfitReceiverEntity> {
|
||||
|
||||
/**
|
||||
* 分页查询分账接收方
|
||||
*/
|
||||
PageUtils queryPage(Map<String, Object> params);
|
||||
|
||||
/**
|
||||
* 根据ID获取分账接收方
|
||||
*/
|
||||
DayCardProfitReceiverEntity getById(Long id);
|
||||
|
||||
/**
|
||||
* 新增分账接收方
|
||||
*/
|
||||
void saveReceiver(DayCardProfitReceiverEntity receiver);
|
||||
|
||||
/**
|
||||
* 修改分账接收方
|
||||
*/
|
||||
void updateReceiver(DayCardProfitReceiverEntity receiver);
|
||||
|
||||
/**
|
||||
* 批量删除分账接收方(软删除)
|
||||
*/
|
||||
void delete(Long[] ids);
|
||||
|
||||
/**
|
||||
* 同步接收方到微信(占位方法,实际微信API待对接)
|
||||
*
|
||||
* @param id 接收方ID
|
||||
*/
|
||||
void syncToWechat(Long id);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package io.as.modules.daycard.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import io.as.common.utils.PageUtils;
|
||||
import io.as.modules.daycard.entity.DayCardProfitRuleEntity;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 天卡分润规则 Service
|
||||
*/
|
||||
public interface DayCardProfitRuleService extends IService<DayCardProfitRuleEntity> {
|
||||
|
||||
/**
|
||||
* 分页查询分润规则
|
||||
*/
|
||||
PageUtils queryPage(Map<String, Object> params);
|
||||
|
||||
/**
|
||||
* 根据ID获取分润规则
|
||||
*/
|
||||
DayCardProfitRuleEntity getById(Long id);
|
||||
|
||||
/**
|
||||
* 新增分润规则(30%校验)
|
||||
*/
|
||||
void saveRule(DayCardProfitRuleEntity rule);
|
||||
|
||||
/**
|
||||
* 修改分润规则(30%校验)
|
||||
*/
|
||||
void updateRule(DayCardProfitRuleEntity rule);
|
||||
|
||||
/**
|
||||
* 批量删除分润规则(软删除)
|
||||
*/
|
||||
void delete(Long[] ids);
|
||||
|
||||
/**
|
||||
* 逐级匹配分润规则
|
||||
* <p>优先门店级 -> 渠道级 -> 合作商级</p>
|
||||
*
|
||||
* @param partnerId 合作伙伴ID
|
||||
* @param channelOrgId 渠道组织ID
|
||||
* @param storeOrgId 门店组织ID(可空)
|
||||
* @return 匹配的分润规则列表,按 priority DESC 排序
|
||||
*/
|
||||
List<DayCardProfitRuleEntity> matchRules(Long partnerId, Long channelOrgId, Long storeOrgId);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package io.as.modules.daycard.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import io.as.common.utils.PageUtils;
|
||||
import io.as.modules.daycard.entity.DayCardRefundEntity;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 天卡退款 Service
|
||||
*/
|
||||
public interface DayCardRefundService extends IService<DayCardRefundEntity> {
|
||||
|
||||
/**
|
||||
* 分页查询退款记录
|
||||
*/
|
||||
PageUtils queryPage(Map<String, Object> params);
|
||||
|
||||
/**
|
||||
* 根据退款单号查询退款记录
|
||||
*
|
||||
* @param refundNo 退款单号
|
||||
* @return 退款记录
|
||||
*/
|
||||
DayCardRefundEntity getByRefundNo(String refundNo);
|
||||
|
||||
/**
|
||||
* 发起退款
|
||||
* <p>流程:校验订单PAID -> 生成退款单号 -> 创建退款记录 -> 释放卡 -> 回退分账 -> 微信退款 -> 更新退款成功</p>
|
||||
*
|
||||
* @param orderNo 订单号
|
||||
* @param reason 退款原因
|
||||
* @return 退款记录
|
||||
*/
|
||||
DayCardRefundEntity refund(String orderNo, String reason);
|
||||
|
||||
/**
|
||||
* 更新退款状态
|
||||
*
|
||||
* @param id 退款记录ID
|
||||
* @param status 退款状态
|
||||
* @param errorMessage 错误信息(可为空)
|
||||
*/
|
||||
void updateStatus(Long id, String status, String errorMessage);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package io.as.modules.daycard.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import io.as.common.utils.PageUtils;
|
||||
import io.as.modules.daycard.entity.DayCardSettleOrderEntity;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 天卡分账单 Service
|
||||
*/
|
||||
public interface DayCardSettleOrderService extends IService<DayCardSettleOrderEntity> {
|
||||
|
||||
/**
|
||||
* 分页查询分账单(支持 orderNo/status 筛选)
|
||||
*/
|
||||
PageUtils queryPage(Map<String, Object> params);
|
||||
|
||||
/**
|
||||
* 根据分账单号获取分账单
|
||||
*/
|
||||
DayCardSettleOrderEntity getBySettleNo(String settleNo);
|
||||
|
||||
/**
|
||||
* 保存分账单
|
||||
*/
|
||||
void saveSettleOrder(DayCardSettleOrderEntity entity);
|
||||
|
||||
/**
|
||||
* 更新分账单状态
|
||||
*
|
||||
* @param id 分账单ID
|
||||
* @param status 状态
|
||||
* @param errorMessage 错误信息(可空)
|
||||
*/
|
||||
void updateStatus(Long id, String status, String errorMessage);
|
||||
|
||||
/**
|
||||
* 根据订单号获取分账单列表
|
||||
*/
|
||||
List<DayCardSettleOrderEntity> getByOrderNo(String orderNo);
|
||||
|
||||
/**
|
||||
* 重试失败的分账单
|
||||
*
|
||||
* @return 重试的分账单数量
|
||||
*/
|
||||
int retryFailed();
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package io.as.modules.daycard.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import io.as.common.utils.PageUtils;
|
||||
import io.as.modules.daycard.entity.DayCardSettleRecordEntity;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 天卡分账记录 Service
|
||||
*/
|
||||
public interface DayCardSettleRecordService extends IService<DayCardSettleRecordEntity> {
|
||||
|
||||
/**
|
||||
* 分页查询分账记录
|
||||
*/
|
||||
PageUtils queryPage(Map<String, Object> params);
|
||||
|
||||
/**
|
||||
* 根据ID获取分账记录
|
||||
*/
|
||||
DayCardSettleRecordEntity getById(Long id);
|
||||
|
||||
/**
|
||||
* 新增分账记录
|
||||
*/
|
||||
void saveRecord(DayCardSettleRecordEntity record);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package io.as.modules.daycard.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import io.as.common.utils.PageUtils;
|
||||
import io.as.modules.daycard.entity.DayCardSettleRuleEntity;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 天卡分账规则 Service
|
||||
*/
|
||||
public interface DayCardSettleRuleService extends IService<DayCardSettleRuleEntity> {
|
||||
|
||||
/**
|
||||
* 分页查询分账规则
|
||||
*/
|
||||
PageUtils queryPage(Map<String, Object> params);
|
||||
|
||||
/**
|
||||
* 根据ID获取分账规则
|
||||
*/
|
||||
DayCardSettleRuleEntity getById(Long id);
|
||||
|
||||
/**
|
||||
* 新增分账规则
|
||||
*/
|
||||
void saveRule(DayCardSettleRuleEntity rule);
|
||||
|
||||
/**
|
||||
* 修改分账规则
|
||||
*/
|
||||
void update(DayCardSettleRuleEntity rule);
|
||||
|
||||
/**
|
||||
* 批量删除分账规则(软删除)
|
||||
*/
|
||||
void delete(Long[] ids);
|
||||
|
||||
/**
|
||||
* 根据订单信息获取适用的分账规则
|
||||
* 优先匹配门店级规则,其次渠道级规则
|
||||
*
|
||||
* @param partnerId 合作伙伴ID
|
||||
* @param channelOrgId 渠道组织ID
|
||||
* @param storeOrgId 门店组织ID(可空)
|
||||
* @return 适用的分账规则列表
|
||||
*/
|
||||
List<DayCardSettleRuleEntity> getRulesByOrder(Long partnerId, Long channelOrgId, Long storeOrgId);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package io.as.modules.daycard.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import io.as.common.utils.PageUtils;
|
||||
import io.as.modules.daycard.entity.OrgSettlementAccountEntity;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 组织分账接收方 Service
|
||||
*/
|
||||
public interface OrgSettlementAccountService extends IService<OrgSettlementAccountEntity> {
|
||||
|
||||
/**
|
||||
* 分页查询(支持 orgId 筛选)
|
||||
*/
|
||||
PageUtils queryPage(Map<String, Object> params);
|
||||
|
||||
/**
|
||||
* 根据 ID 获取
|
||||
*/
|
||||
OrgSettlementAccountEntity getById(Long id);
|
||||
|
||||
/**
|
||||
* 保存接收方(设置 deleteFlag=UNDEL)
|
||||
*/
|
||||
void saveAccount(OrgSettlementAccountEntity account);
|
||||
|
||||
/**
|
||||
* 批量删除(软删除)
|
||||
*/
|
||||
void delete(Long[] ids);
|
||||
|
||||
/**
|
||||
* 同步接收方到微信(占位:设置 bind_status=ACTIVE)
|
||||
*/
|
||||
void syncToWechat(Long id);
|
||||
|
||||
/**
|
||||
* 根据组织 ID 获取已绑定的接收方
|
||||
*/
|
||||
OrgSettlementAccountEntity getByOrgId(Long orgId);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package io.as.modules.daycard.service;
|
||||
|
||||
import io.as.modules.daycard.entity.DayCardOrderEntity;
|
||||
import io.as.modules.daycard.entity.DayCardSettleOrderEntity;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 分润计算 Service
|
||||
*/
|
||||
public interface ProfitCalcService {
|
||||
|
||||
/**
|
||||
* 试算分账结果
|
||||
*
|
||||
* @param partnerId 合作伙伴ID
|
||||
* @param channelOrgId 渠道组织ID
|
||||
* @param storeOrgId 门店组织ID
|
||||
* @param totalAmount 总金额
|
||||
* @return Map: { totalAmount, rules: [{ruleId, receiverAccount, receiverName, shareRatio, settleAmount}], totalSettleAmount }
|
||||
*/
|
||||
Map<String, Object> calculate(Long partnerId, Long channelOrgId, Long storeOrgId, BigDecimal totalAmount);
|
||||
|
||||
/**
|
||||
* 匹配规则并计算分账金额,构建分账单列表(不保存,由调用方保存)
|
||||
*
|
||||
* @param orderNo 订单号
|
||||
* @param order 天卡订单
|
||||
* @return 分账单列表
|
||||
*/
|
||||
List<DayCardSettleOrderEntity> buildSettleOrders(String orderNo, DayCardOrderEntity order);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package io.as.modules.daycard.service;
|
||||
|
||||
/**
|
||||
* 分账执行 Service
|
||||
*/
|
||||
public interface ProfitSharingExecuteService {
|
||||
|
||||
/**
|
||||
* 执行分账(支付成功后调用)
|
||||
*
|
||||
* @param orderNo 订单号
|
||||
*/
|
||||
void executeSharing(String orderNo);
|
||||
|
||||
/**
|
||||
* 重试失败的分账单
|
||||
*/
|
||||
void retryFailed();
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package io.as.modules.daycard.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import io.as.common.utils.PageUtils;
|
||||
import io.as.modules.daycard.entity.ProfitSharingRuleEntity;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 分账规则 Service
|
||||
*/
|
||||
public interface ProfitSharingRuleService extends IService<ProfitSharingRuleEntity> {
|
||||
|
||||
/**
|
||||
* 分页查询分润规则
|
||||
*/
|
||||
PageUtils queryPage(Map<String, Object> params);
|
||||
|
||||
/**
|
||||
* 获取规则详情(含 items 子表)
|
||||
*/
|
||||
ProfitSharingRuleEntity getInfo(Long id);
|
||||
|
||||
/**
|
||||
* 新增分润规则
|
||||
*/
|
||||
void saveRule(ProfitSharingRuleEntity rule);
|
||||
|
||||
/**
|
||||
* 修改分润规则
|
||||
*/
|
||||
void updateRule(ProfitSharingRuleEntity rule);
|
||||
|
||||
/**
|
||||
* 删除分润规则(逻辑删除)
|
||||
*/
|
||||
void delete(Long id);
|
||||
|
||||
/**
|
||||
* 匹配分账规则
|
||||
*
|
||||
* @param storeOrgId 门店组织ID
|
||||
* @param agentOrgId 代理组织ID
|
||||
* @param channelOrgId 渠道组织ID
|
||||
* @return 匹配的分账规则(含规则项),无匹配返回 null
|
||||
*/
|
||||
ProfitSharingRuleEntity matchRule(Long storeOrgId, Long agentOrgId, Long channelOrgId);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package io.as.modules.daycard.service;
|
||||
|
||||
import io.as.modules.daycard.entity.DayCardSettleOrderEntity;
|
||||
|
||||
/**
|
||||
* 分账执行 Service
|
||||
*/
|
||||
public interface SettleExecuteService {
|
||||
|
||||
/**
|
||||
* 执行分账(支付成功后调用)
|
||||
* <p>流程:查询订单 -> 构建分账单 -> 保存 -> 逐条执行</p>
|
||||
*
|
||||
* @param orderNo 订单号
|
||||
*/
|
||||
void executeSettle(String orderNo);
|
||||
|
||||
/**
|
||||
* 执行单条分账单
|
||||
*
|
||||
* @param settleOrder 分账单
|
||||
*/
|
||||
void executeSingle(DayCardSettleOrderEntity settleOrder);
|
||||
|
||||
/**
|
||||
* 重试失败的分账单
|
||||
*
|
||||
* @return 重试的分账单数量
|
||||
*/
|
||||
int retryFailed();
|
||||
}
|
||||
+174
@@ -0,0 +1,174 @@
|
||||
package io.as.modules.daycard.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import io.as.common.exception.ASException;
|
||||
import io.as.modules.daycard.constants.DayCardConstants;
|
||||
import io.as.modules.daycard.dao.DayCardActivationCodeDao;
|
||||
import io.as.modules.daycard.entity.DayCardActivationCodeEntity;
|
||||
import io.as.modules.daycard.service.DayCardActivationCodeService;
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
|
||||
/**
|
||||
* 天卡设备激活码服务实现
|
||||
* <p>激活码生成算法复用生产环境 as-partner-backend:5位随机字符(字符集 ABCDEFGHJKMNPQRSTUVWXY23456789,去除易混字符 0/I/L/Z/O/1)</p>
|
||||
*/
|
||||
@Service("dayCardActivationCodeService")
|
||||
public class DayCardActivationCodeServiceImpl
|
||||
extends ServiceImpl<DayCardActivationCodeDao, DayCardActivationCodeEntity>
|
||||
implements DayCardActivationCodeService {
|
||||
|
||||
/** 激活码随机字符集:去除 0/I/L/Z/O/1 等易混字符 */
|
||||
private static final String ALLOWED_CHARS = "ABCDEFGHJKMNPQRSTUVWXY23456789";
|
||||
private static final int CODE_LENGTH = 5;
|
||||
private static final int MAX_RETRY = 64;
|
||||
|
||||
@Override
|
||||
public DayCardActivationCodeEntity getByCode(String code) {
|
||||
if (StringUtils.isBlank(code)) {
|
||||
return null;
|
||||
}
|
||||
return this.lambdaQuery()
|
||||
.eq(DayCardActivationCodeEntity::getCode, code.trim())
|
||||
.eq(DayCardActivationCodeEntity::getDeleteFlag, DayCardConstants.DELETE_FLAG_UNDEL)
|
||||
.eq(DayCardActivationCodeEntity::getStatus, "ACTIVE")
|
||||
.last("LIMIT 1")
|
||||
.one();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<DayCardActivationCodeEntity> listByStoreOrgId(Long storeOrgId) {
|
||||
if (storeOrgId == null) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
return this.lambdaQuery()
|
||||
.eq(DayCardActivationCodeEntity::getStoreOrgId, storeOrgId)
|
||||
.eq(DayCardActivationCodeEntity::getDeleteFlag, DayCardConstants.DELETE_FLAG_UNDEL)
|
||||
.orderByDesc(DayCardActivationCodeEntity::getId)
|
||||
.list();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public DayCardActivationCodeEntity generateForStore(Long storeOrgId, Long channelOrgId, String remark) {
|
||||
if (storeOrgId == null) {
|
||||
throw new ASException("门店组织ID不能为空");
|
||||
}
|
||||
// 检查门店是否已有激活码,已有则返回已有的
|
||||
DayCardActivationCodeEntity existing = this.lambdaQuery()
|
||||
.eq(DayCardActivationCodeEntity::getStoreOrgId, storeOrgId)
|
||||
.eq(DayCardActivationCodeEntity::getDeleteFlag, DayCardConstants.DELETE_FLAG_UNDEL)
|
||||
.last("LIMIT 1")
|
||||
.one();
|
||||
if (existing != null) {
|
||||
return existing;
|
||||
}
|
||||
|
||||
Date now = new Date();
|
||||
DayCardActivationCodeEntity entity = new DayCardActivationCodeEntity();
|
||||
entity.setCode(generateUniqueCode(null));
|
||||
entity.setChannelOrgId(channelOrgId);
|
||||
entity.setStoreOrgId(storeOrgId);
|
||||
entity.setStatus("ACTIVE");
|
||||
entity.setMaxDeviceCount(0);
|
||||
entity.setActivatedCount(0);
|
||||
entity.setRemark(remark);
|
||||
entity.setDeleteFlag(DayCardConstants.DELETE_FLAG_UNDEL);
|
||||
entity.setCreatedAt(now);
|
||||
entity.setUpdatedAt(now);
|
||||
this.save(entity);
|
||||
return entity;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public String regenerateCode(Long id) {
|
||||
if (id == null) {
|
||||
throw new ASException("激活码记录ID不能为空");
|
||||
}
|
||||
DayCardActivationCodeEntity entity = getById(id);
|
||||
if (entity == null) {
|
||||
throw new ASException("激活码记录不存在");
|
||||
}
|
||||
String newCode = generateUniqueCode(id);
|
||||
entity.setCode(newCode);
|
||||
entity.setUpdatedAt(new Date());
|
||||
updateById(entity);
|
||||
return newCode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateStatus(Long id, String status) {
|
||||
if (id == null) {
|
||||
throw new ASException("激活码记录ID不能为空");
|
||||
}
|
||||
if (!"ACTIVE".equals(status) && !"DISABLED".equals(status)) {
|
||||
throw new ASException("无效的状态值");
|
||||
}
|
||||
DayCardActivationCodeEntity entity = getById(id);
|
||||
if (entity == null) {
|
||||
throw new ASException("激活码记录不存在");
|
||||
}
|
||||
entity.setStatus(status);
|
||||
entity.setUpdatedAt(new Date());
|
||||
updateById(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(Long id) {
|
||||
if (id == null) {
|
||||
throw new ASException("激活码记录ID不能为空");
|
||||
}
|
||||
DayCardActivationCodeEntity entity = getById(id);
|
||||
if (entity == null) {
|
||||
throw new ASException("激活码记录不存在");
|
||||
}
|
||||
entity.setDeleteFlag("DEL");
|
||||
entity.setUpdatedAt(new Date());
|
||||
updateById(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void incrementActivatedCount(Long id) {
|
||||
if (id == null) {
|
||||
return;
|
||||
}
|
||||
DayCardActivationCodeEntity entity = getById(id);
|
||||
if (entity == null) {
|
||||
return;
|
||||
}
|
||||
int current = entity.getActivatedCount() != null ? entity.getActivatedCount() : 0;
|
||||
entity.setActivatedCount(current + 1);
|
||||
entity.setUpdatedAt(new Date());
|
||||
updateById(entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成5位随机字符激活码,校验全局唯一(排除自身ID)
|
||||
*/
|
||||
private String generateUniqueCode(Long excludeId) {
|
||||
Random random = new Random();
|
||||
for (int attempt = 0; attempt < MAX_RETRY; attempt++) {
|
||||
StringBuilder sb = new StringBuilder(CODE_LENGTH);
|
||||
for (int i = 0; i < CODE_LENGTH; i++) {
|
||||
sb.append(ALLOWED_CHARS.charAt(random.nextInt(ALLOWED_CHARS.length())));
|
||||
}
|
||||
String candidate = sb.toString();
|
||||
long dup = this.lambdaQuery()
|
||||
.eq(DayCardActivationCodeEntity::getCode, candidate)
|
||||
.eq(DayCardActivationCodeEntity::getDeleteFlag, DayCardConstants.DELETE_FLAG_UNDEL)
|
||||
.ne(excludeId != null, DayCardActivationCodeEntity::getId, excludeId)
|
||||
.count();
|
||||
if (dup == 0) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
throw new ASException("生成唯一激活码失败,请重试");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,802 @@
|
||||
package io.as.modules.daycard.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import io.as.common.exception.ASException;
|
||||
import io.as.common.utils.PageUtils;
|
||||
import io.as.common.utils.Query;
|
||||
import io.as.common.utils.RedisUtils;
|
||||
import io.as.modules.daycard.constants.DayCardConstants;
|
||||
import io.as.modules.daycard.dao.DayCardOrderDao;
|
||||
import io.as.modules.daycard.dao.DayCardPoolDao;
|
||||
import io.as.modules.daycard.entity.DayCardOrderEntity;
|
||||
import io.as.modules.daycard.entity.DayCardPoolEntity;
|
||||
import io.as.modules.daycard.entity.DayCardPriceConfigEntity;
|
||||
import io.as.modules.daycard.service.DayCardOrderService;
|
||||
import io.as.modules.daycard.service.DayCardPriceConfigService;
|
||||
import io.as.modules.daycard.service.ProfitSharingExecuteService;
|
||||
import io.as.modules.daycard.service.SettleExecuteService;
|
||||
import io.as.modules.device.dao.CardDeviceBindingDao;
|
||||
import io.as.modules.device.dao.DeviceDao;
|
||||
import io.as.modules.device.dto.DeviceOrgContext;
|
||||
import io.as.modules.device.entity.DeviceEntity;
|
||||
import io.as.modules.device.service.DeviceResolveService;
|
||||
import io.as.modules.openapi.sign.ApiDeviceSignService;
|
||||
import io.as.modules.jst.service.JushitingApiService;
|
||||
import io.as.modules.jst.service.JstActivateCardResult;
|
||||
import io.as.modules.payment.service.WechatPayService;
|
||||
import io.as.modules.sys.entity.SysOrgEntity;
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
|
||||
/**
|
||||
* 天卡订单 Service 实现
|
||||
*/
|
||||
@Service("dayCardOrderService")
|
||||
public class DayCardOrderServiceImpl extends ServiceImpl<DayCardOrderDao, DayCardOrderEntity> implements DayCardOrderService {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(DayCardOrderServiceImpl.class);
|
||||
|
||||
@Autowired
|
||||
private DayCardPoolDao dayCardPoolDao;
|
||||
@Autowired
|
||||
private RedisUtils redisUtils;
|
||||
@Autowired
|
||||
private JushitingApiService jushitingApiService;
|
||||
@Autowired
|
||||
private DeviceDao deviceDao;
|
||||
@Autowired
|
||||
private CardDeviceBindingDao cardDeviceBindingDao;
|
||||
@Autowired
|
||||
private DayCardPriceConfigService dayCardPriceConfigService;
|
||||
@Autowired
|
||||
private DeviceResolveService deviceResolveService;
|
||||
@Autowired
|
||||
@Lazy
|
||||
private SettleExecuteService settleExecuteService;
|
||||
@Autowired
|
||||
@Lazy
|
||||
private ProfitSharingExecuteService profitSharingExecuteService;
|
||||
@Autowired
|
||||
@Lazy
|
||||
private WechatPayService wechatPayService;
|
||||
|
||||
/** 自注入以确保 @Async 通过 Spring 代理生效(避免同类自调用绕过 AOP 代理) */
|
||||
@Autowired
|
||||
@Lazy
|
||||
private DayCardOrderService selfProxy;
|
||||
|
||||
@Autowired
|
||||
private ApiDeviceSignService apiDeviceSignService;
|
||||
|
||||
@Value("${renren.daycard.order-timeout-minutes:15}")
|
||||
private int orderTimeoutMinutes;
|
||||
|
||||
@Value("${renren.daycard.cancel-batch-size:200}")
|
||||
private int cancelBatchSize;
|
||||
|
||||
@Override
|
||||
public PageUtils queryPage(Map<String, Object> params, Long partnerId) {
|
||||
IPage<DayCardOrderEntity> page = new Query<DayCardOrderEntity>().getPage(params, "created_at", false);
|
||||
|
||||
QueryWrapper<DayCardOrderEntity> wrapper = new QueryWrapper<>();
|
||||
wrapper.eq("delete_flag", DayCardConstants.DELETE_FLAG_UNDEL);
|
||||
if (partnerId != null) {
|
||||
wrapper.eq("partner_id", partnerId);
|
||||
}
|
||||
String orderNo = (String) params.get("orderNo");
|
||||
if (StringUtils.isNotBlank(orderNo)) {
|
||||
wrapper.eq("order_no", orderNo);
|
||||
}
|
||||
String status = (String) params.get("status");
|
||||
if (StringUtils.isNotBlank(status)) {
|
||||
wrapper.eq("status", status);
|
||||
}
|
||||
|
||||
IPage<DayCardOrderEntity> result = baseMapper.selectPage(page, wrapper);
|
||||
return new PageUtils(result);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public DayCardOrderEntity createOrder(Long deviceId) {
|
||||
if (deviceId == null) {
|
||||
throw new ASException("设备ID不能为空");
|
||||
}
|
||||
|
||||
// 0. 查重:已有有效权益则拒绝创建订单,避免卡密浪费
|
||||
DayCardPoolEntity existingBenefits = getDeviceBenefits(deviceId);
|
||||
if (existingBenefits != null) {
|
||||
throw new ASException("该设备已有有效天卡权益,无需重复购买");
|
||||
}
|
||||
|
||||
// 1. Redis防重:用 deviceId 作为锁键
|
||||
String lockKey = DayCardConstants.REDIS_DEVICE_ORDER_PREFIX + deviceId;
|
||||
long ttlSeconds = orderTimeoutMinutes * 60L;
|
||||
boolean locked = redisUtils.setIfAbsent(lockKey, "1", ttlSeconds);
|
||||
if (!locked) {
|
||||
throw new ASException("该设备已有进行中的订单,请稍后再试");
|
||||
}
|
||||
|
||||
try {
|
||||
// 2. 解析设备组织链路(设备 → 门店 → 渠道 → 代理商)
|
||||
DeviceOrgContext orgContext = deviceResolveService.resolveDevice(deviceId);
|
||||
DeviceEntity device = orgContext.getDevice();
|
||||
Long storeOrgId = device.getStoreOrgId();
|
||||
Long channelOrgId = orgContext.getChannelOrg() != null ? orgContext.getChannelOrg().getId() : null;
|
||||
Long agentOrgId = orgContext.getAgentOrg() != null ? orgContext.getAgentOrg().getId() : null;
|
||||
|
||||
// 3. 组织链路快照
|
||||
SysOrgEntity storeOrg = orgContext.getStoreOrg();
|
||||
String orgPathSnapshot = storeOrg != null ? storeOrg.getMpath() : null;
|
||||
|
||||
// 4. 3 层定价:门店 > 渠道 > 平台默认
|
||||
DayCardPriceConfigEntity priceConfig = dayCardPriceConfigService.getEffectivePrice(channelOrgId, storeOrgId);
|
||||
if (priceConfig == null) {
|
||||
throw new ASException("未配置定价策略,请先在管理后台配置天卡价格");
|
||||
}
|
||||
BigDecimal amount = priceConfig.getPrice();
|
||||
|
||||
// 5. 生成订单号
|
||||
String orderNo = generateOrderNo();
|
||||
|
||||
// 6. 插入订单(partnerId 置 null)
|
||||
DayCardOrderEntity order = new DayCardOrderEntity();
|
||||
order.setOrderNo(orderNo);
|
||||
order.setPartnerId(null);
|
||||
order.setDeviceId(device.getId());
|
||||
order.setStoreOrgId(storeOrgId);
|
||||
order.setChannelOrgId(channelOrgId);
|
||||
order.setAmount(amount);
|
||||
order.setStatus(DayCardConstants.ORDER_STATUS_CREATED);
|
||||
order.setPayChannel("MOCK");
|
||||
order.setDeleteFlag(DayCardConstants.DELETE_FLAG_UNDEL);
|
||||
Date now = new Date();
|
||||
order.setCreatedAt(now);
|
||||
order.setUpdatedAt(now);
|
||||
Date expireTime = new Date(now.getTime() + ttlSeconds * 1000);
|
||||
order.setExpireTime(expireTime);
|
||||
order.setDeviceSn(device.getDeviceSn());
|
||||
order.setMac(device.getMac());
|
||||
order.setAgentOrgId(agentOrgId);
|
||||
order.setOrgPathSnapshot(orgPathSnapshot);
|
||||
baseMapper.insert(order);
|
||||
|
||||
// 7. 微信 Native 支付下单
|
||||
if (wechatPayService.isConfigured()) {
|
||||
try {
|
||||
String codeUrl = wechatPayService.createNativePay(orderNo, "天卡订单", amount);
|
||||
order.setCodeUrl(codeUrl);
|
||||
order.setPayChannel("WECHAT");
|
||||
UpdateWrapper<DayCardOrderEntity> payUpdate = new UpdateWrapper<>();
|
||||
payUpdate.eq("id", order.getId())
|
||||
.set("code_url", codeUrl)
|
||||
.set("pay_channel", "WECHAT");
|
||||
baseMapper.update(null, payUpdate);
|
||||
} catch (Exception e) {
|
||||
logger.warn("微信下单失败,回退到MOCK模式: orderNo={}", orderNo, e);
|
||||
order.setPayChannel("MOCK");
|
||||
order.setCodeUrl("mock://pay?orderNo=" + orderNo);
|
||||
UpdateWrapper<DayCardOrderEntity> payUpdate = new UpdateWrapper<>();
|
||||
payUpdate.eq("id", order.getId())
|
||||
.set("code_url", order.getCodeUrl())
|
||||
.set("pay_channel", "MOCK");
|
||||
baseMapper.update(null, payUpdate);
|
||||
}
|
||||
} else {
|
||||
order.setPayChannel("MOCK");
|
||||
order.setCodeUrl("mock://pay?orderNo=" + orderNo);
|
||||
UpdateWrapper<DayCardOrderEntity> payUpdate = new UpdateWrapper<>();
|
||||
payUpdate.eq("id", order.getId())
|
||||
.set("code_url", order.getCodeUrl())
|
||||
.set("pay_channel", "MOCK");
|
||||
baseMapper.update(null, payUpdate);
|
||||
}
|
||||
|
||||
// 8. SKIP LOCKED 抢卡(通用池,不按 partner 过滤)
|
||||
DayCardPoolEntity pool = dayCardPoolDao.selectOneForUpdateSkipLocked();
|
||||
if (pool == null) {
|
||||
logger.warn("创建天卡订单失败:天卡池库存不足, orderNo={}", orderNo);
|
||||
throw new ASException("天卡库存不足,请补充库存后再试");
|
||||
}
|
||||
|
||||
// 9. 乐观锁更新卡状态 UNUSED -> RESERVED
|
||||
UpdateWrapper<DayCardPoolEntity> poolUpdate = new UpdateWrapper<>();
|
||||
poolUpdate.eq("id", pool.getId())
|
||||
.in("status", DayCardConstants.POOL_STATUS_UNUSED, DayCardConstants.POOL_STATUS_REUSABLE)
|
||||
.eq("delete_flag", DayCardConstants.DELETE_FLAG_UNDEL)
|
||||
.set("status", DayCardConstants.POOL_STATUS_RESERVED)
|
||||
.set("order_no", orderNo)
|
||||
.set("device_id", device.getId())
|
||||
.set("updated_at", now);
|
||||
int updated = dayCardPoolDao.update(null, poolUpdate);
|
||||
if (updated == 0) {
|
||||
logger.warn("抢卡竞争失败: poolId={}, orderNo={}", pool.getId(), orderNo);
|
||||
throw new ASException("天卡库存不足,请稍后再试");
|
||||
}
|
||||
|
||||
order.setCardNo(pool.getCardCode());
|
||||
return order;
|
||||
} catch (ASException e) {
|
||||
redisUtils.delete(lockKey);
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
redisUtils.delete(lockKey);
|
||||
throw new ASException("创建天卡订单失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public DayCardOrderEntity createOrder(String mac, String deviceSn, String deviceUid, Long partnerId) {
|
||||
// 1. Redis防重:用 mac 或 deviceSn 或 deviceUid 作为锁键
|
||||
String lockKey = DayCardConstants.REDIS_DEVICE_ORDER_PREFIX;
|
||||
if (StringUtils.isNotBlank(mac)) {
|
||||
lockKey = lockKey + mac;
|
||||
} else if (StringUtils.isNotBlank(deviceSn)) {
|
||||
lockKey = lockKey + deviceSn;
|
||||
} else if (StringUtils.isNotBlank(deviceUid)) {
|
||||
lockKey = lockKey + deviceUid;
|
||||
} else {
|
||||
throw new ASException("设备标识不能全为空");
|
||||
}
|
||||
long ttlSeconds = orderTimeoutMinutes * 60L;
|
||||
boolean locked = redisUtils.setIfAbsent(lockKey, "1", ttlSeconds);
|
||||
if (!locked) {
|
||||
throw new ASException("该设备已有进行中的订单,请稍后再试");
|
||||
}
|
||||
|
||||
try {
|
||||
// 2. 通过 DeviceResolveService 解析设备组织链路(设备 → 门店 → 渠道 → 代理商)
|
||||
DeviceOrgContext orgContext = deviceResolveService.resolveDevice(mac, deviceSn, deviceUid);
|
||||
DeviceEntity device = orgContext.getDevice();
|
||||
Long storeOrgId = device.getStoreOrgId();
|
||||
// 查重:已有有效权益则拒绝创建订单,避免卡密浪费
|
||||
DayCardPoolEntity existingBenefits = getDeviceBenefits(device.getId());
|
||||
if (existingBenefits != null) {
|
||||
throw new ASException("该设备已有有效天卡权益,无需重复购买");
|
||||
}
|
||||
Long channelOrgId = orgContext.getChannelOrg() != null ? orgContext.getChannelOrg().getId() : null;
|
||||
Long agentOrgId = orgContext.getAgentOrg() != null ? orgContext.getAgentOrg().getId() : null;
|
||||
|
||||
// 3. 构建组织链路快照(使用门店的 mpath)
|
||||
SysOrgEntity storeOrg = orgContext.getStoreOrg();
|
||||
String orgPathSnapshot = storeOrg != null ? storeOrg.getMpath() : null;
|
||||
|
||||
// 4. 3 层定价:门店 > 渠道 > 平台默认
|
||||
DayCardPriceConfigEntity priceConfig = dayCardPriceConfigService.getEffectivePrice(channelOrgId, storeOrgId);
|
||||
if (priceConfig == null) {
|
||||
throw new ASException("未配置定价策略,请先在管理后台配置天卡价格");
|
||||
}
|
||||
BigDecimal amount = priceConfig.getPrice();
|
||||
|
||||
// 5. 生成订单号(yyyyMMddHHmmss + 6位随机数)
|
||||
String orderNo = generateOrderNo();
|
||||
|
||||
// 6. 插入订单
|
||||
DayCardOrderEntity order = new DayCardOrderEntity();
|
||||
order.setOrderNo(orderNo);
|
||||
order.setPartnerId(partnerId);
|
||||
order.setDeviceId(device.getId());
|
||||
order.setStoreOrgId(storeOrgId);
|
||||
order.setChannelOrgId(channelOrgId);
|
||||
order.setAmount(amount);
|
||||
order.setStatus(DayCardConstants.ORDER_STATUS_CREATED);
|
||||
order.setPayChannel("MOCK");
|
||||
order.setDeleteFlag(DayCardConstants.DELETE_FLAG_UNDEL);
|
||||
Date now = new Date();
|
||||
order.setCreatedAt(now);
|
||||
order.setUpdatedAt(now);
|
||||
// 订单过期时间
|
||||
Date expireTime = new Date(now.getTime() + ttlSeconds * 1000);
|
||||
order.setExpireTime(expireTime);
|
||||
// 额外设置设备冗余信息
|
||||
order.setDeviceSn(device.getDeviceSn());
|
||||
order.setMac(device.getMac());
|
||||
order.setAgentOrgId(agentOrgId);
|
||||
order.setOrgPathSnapshot(orgPathSnapshot);
|
||||
baseMapper.insert(order);
|
||||
|
||||
// 6.5 微信 Native 支付下单
|
||||
if (wechatPayService.isConfigured()) {
|
||||
try {
|
||||
String codeUrl = wechatPayService.createNativePay(orderNo, "天卡订单", amount);
|
||||
order.setCodeUrl(codeUrl);
|
||||
order.setPayChannel("WECHAT");
|
||||
UpdateWrapper<DayCardOrderEntity> payUpdate = new UpdateWrapper<>();
|
||||
payUpdate.eq("id", order.getId())
|
||||
.set("code_url", codeUrl)
|
||||
.set("pay_channel", "WECHAT");
|
||||
baseMapper.update(null, payUpdate);
|
||||
} catch (Exception e) {
|
||||
logger.warn("微信下单失败,回退到MOCK模式: orderNo={}", orderNo, e);
|
||||
order.setPayChannel("MOCK");
|
||||
order.setCodeUrl("mock://pay?orderNo=" + orderNo);
|
||||
UpdateWrapper<DayCardOrderEntity> payUpdate = new UpdateWrapper<>();
|
||||
payUpdate.eq("id", order.getId())
|
||||
.set("code_url", order.getCodeUrl())
|
||||
.set("pay_channel", "MOCK");
|
||||
baseMapper.update(null, payUpdate);
|
||||
}
|
||||
} else {
|
||||
// 开发模式:使用 Mock 支付
|
||||
order.setPayChannel("MOCK");
|
||||
order.setCodeUrl("mock://pay?orderNo=" + orderNo);
|
||||
UpdateWrapper<DayCardOrderEntity> payUpdate = new UpdateWrapper<>();
|
||||
payUpdate.eq("id", order.getId())
|
||||
.set("code_url", order.getCodeUrl())
|
||||
.set("pay_channel", "MOCK");
|
||||
baseMapper.update(null, payUpdate);
|
||||
}
|
||||
|
||||
// 7. SKIP LOCKED 抢卡
|
||||
DayCardPoolEntity pool = dayCardPoolDao.selectOneForUpdateSkipLocked();
|
||||
if (pool == null) {
|
||||
logger.warn("创建天卡订单失败:合作商[{}]天卡池库存不足, orderNo={}", partnerId, orderNo);
|
||||
throw new ASException("天卡库存不足,请补充库存后再试");
|
||||
}
|
||||
|
||||
// 8. 乐观锁更新卡状态 UNUSED -> RESERVED
|
||||
UpdateWrapper<DayCardPoolEntity> poolUpdate = new UpdateWrapper<>();
|
||||
poolUpdate.eq("id", pool.getId())
|
||||
.in("status", DayCardConstants.POOL_STATUS_UNUSED, DayCardConstants.POOL_STATUS_REUSABLE)
|
||||
.eq("delete_flag", DayCardConstants.DELETE_FLAG_UNDEL)
|
||||
.set("status", DayCardConstants.POOL_STATUS_RESERVED)
|
||||
.set("order_no", orderNo)
|
||||
.set("device_id", device.getId())
|
||||
.set("updated_at", now);
|
||||
int updated = dayCardPoolDao.update(null, poolUpdate);
|
||||
if (updated == 0) {
|
||||
// 卡已被其他线程抢占
|
||||
logger.warn("抢卡竞争失败: poolId={}, orderNo={}", pool.getId(), orderNo);
|
||||
throw new ASException("天卡库存不足,请稍后再试");
|
||||
}
|
||||
|
||||
// 更新订单上的卡号信息
|
||||
order.setCardNo(pool.getCardCode());
|
||||
|
||||
return order;
|
||||
} catch (ASException e) {
|
||||
// 业务异常时清理Redis锁,允许用户重试
|
||||
redisUtils.delete(lockKey);
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
redisUtils.delete(lockKey);
|
||||
throw new ASException("创建天卡订单失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void payCallback(String orderNo, String payNo, BigDecimal payAmount) {
|
||||
// 1. 幂等检查:查询订单
|
||||
DayCardOrderEntity order = getByOrderNo(orderNo);
|
||||
if (order == null) {
|
||||
throw new ASException("订单不存在: " + orderNo);
|
||||
}
|
||||
// 已支付则幂等返回
|
||||
if (DayCardConstants.ORDER_STATUS_PAID.equals(order.getStatus())) {
|
||||
logger.info("支付回调幂等返回: orderNo={}, 已为PAID状态", orderNo);
|
||||
return;
|
||||
}
|
||||
// 非CREATED/PAYING状态不允许回调
|
||||
if (!DayCardConstants.ORDER_STATUS_CREATED.equals(order.getStatus())
|
||||
&& !DayCardConstants.ORDER_STATUS_PAYING.equals(order.getStatus())) {
|
||||
throw new ASException("订单状态不允许支付回调: " + order.getStatus());
|
||||
}
|
||||
|
||||
// 2. 乐观锁更新订单为PAID
|
||||
UpdateWrapper<DayCardOrderEntity> orderUpdate = new UpdateWrapper<>();
|
||||
orderUpdate.eq("order_no", orderNo)
|
||||
.in("status", DayCardConstants.ORDER_STATUS_CREATED, DayCardConstants.ORDER_STATUS_PAYING)
|
||||
.eq("delete_flag", DayCardConstants.DELETE_FLAG_UNDEL)
|
||||
.set("status", DayCardConstants.ORDER_STATUS_PAID)
|
||||
.set("pay_no", payNo)
|
||||
.set("pay_amount", payAmount)
|
||||
.set("pay_time", new Date())
|
||||
.set("updated_at", new Date());
|
||||
int updated = baseMapper.update(null, orderUpdate);
|
||||
if (updated == 0) {
|
||||
throw new ASException("订单状态已变更,支付回调失败: " + orderNo);
|
||||
}
|
||||
|
||||
// 3. 更新卡状态 RESERVED -> USED,同时设置初始 expire_at(24小时兜底,异步激活成功后覆盖)
|
||||
Date now = new Date();
|
||||
Date defaultExpireAt = new Date(now.getTime() + 24 * 60 * 60 * 1000L);
|
||||
UpdateWrapper<DayCardPoolEntity> poolUpdate = new UpdateWrapper<>();
|
||||
poolUpdate.eq("order_no", orderNo)
|
||||
.eq("status", DayCardConstants.POOL_STATUS_RESERVED)
|
||||
.eq("delete_flag", DayCardConstants.DELETE_FLAG_UNDEL)
|
||||
.set("status", DayCardConstants.POOL_STATUS_USED)
|
||||
.set("activate_status", DayCardConstants.ACTIVATE_STATUS_PENDING)
|
||||
.set("used_at", now)
|
||||
.set("expire_at", defaultExpireAt)
|
||||
.set("updated_at", now);
|
||||
dayCardPoolDao.update(null, poolUpdate);
|
||||
|
||||
// 4. 异步激活(调用jst服务)——通过 selfProxy 调用确保 @Async 生效
|
||||
selfProxy.asyncActivateCard(order);
|
||||
|
||||
// 5. 清理Redis防重锁
|
||||
String lockKey = DayCardConstants.REDIS_DEVICE_ORDER_PREFIX + order.getDeviceId();
|
||||
redisUtils.delete(lockKey);
|
||||
// 兼容新下单接口(锁键使用mac/deviceSn/deviceUid)
|
||||
if (StringUtils.isNotBlank(order.getMac())) {
|
||||
redisUtils.delete(DayCardConstants.REDIS_DEVICE_ORDER_PREFIX + order.getMac());
|
||||
}
|
||||
if (StringUtils.isNotBlank(order.getDeviceSn())) {
|
||||
redisUtils.delete(DayCardConstants.REDIS_DEVICE_ORDER_PREFIX + order.getDeviceSn());
|
||||
}
|
||||
|
||||
// 6. 触发分账(异步)
|
||||
try {
|
||||
profitSharingExecuteService.executeSharing(orderNo);
|
||||
} catch (Exception e) {
|
||||
logger.error("触发分账失败: orderNo={}", orderNo, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 异步激活天卡:调用橘视厅接口
|
||||
* <p>通过 {@link #selfProxy} 调用以确保 Spring AOP 代理生效</p>
|
||||
* <p>激活状态追踪:PENDING → SUCCESS/FAILED</p>
|
||||
*/
|
||||
@Async("thirdPartyActivateExecutor")
|
||||
@Override
|
||||
public void asyncActivateCard(DayCardOrderEntity order) {
|
||||
try {
|
||||
// 查询绑定的天卡
|
||||
QueryWrapper<DayCardPoolEntity> poolWrapper = new QueryWrapper<>();
|
||||
poolWrapper.eq("order_no", order.getOrderNo())
|
||||
.eq("delete_flag", DayCardConstants.DELETE_FLAG_UNDEL);
|
||||
DayCardPoolEntity pool = dayCardPoolDao.selectOne(poolWrapper);
|
||||
if (pool == null) {
|
||||
logger.warn("异步激活天卡失败:未找到订单关联的卡, orderNo={}", order.getOrderNo());
|
||||
return;
|
||||
}
|
||||
|
||||
// 查询设备
|
||||
DeviceEntity device = deviceDao.selectById(order.getDeviceId());
|
||||
if (device == null) {
|
||||
updateActivateStatus(pool.getId(), DayCardConstants.ACTIVATE_STATUS_FAILED, "设备不存在: " + order.getDeviceId(), null);
|
||||
logger.warn("异步激活天卡失败:设备不存在, deviceId={}", order.getDeviceId());
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. 调用橘视厅免密登录获取Token
|
||||
String thirdCardRef = pool.getThirdCardNo();
|
||||
if (StringUtils.isBlank(thirdCardRef)) {
|
||||
updateActivateStatus(pool.getId(), DayCardConstants.ACTIVATE_STATUS_FAILED, "卡的第三方引用为空", null);
|
||||
logger.warn("异步激活天卡失败:卡的第三方引用为空, poolId={}", pool.getId());
|
||||
return;
|
||||
}
|
||||
|
||||
Map<String, String> tokenResult = jushitingApiService.getToken(device, thirdCardRef);
|
||||
if (tokenResult == null || StringUtils.isBlank(tokenResult.get("token"))) {
|
||||
updateActivateStatus(pool.getId(), DayCardConstants.ACTIVATE_STATUS_FAILED, "获取Token失败", null);
|
||||
logger.warn("异步激活天卡失败:获取Token失败, orderNo={}", order.getOrderNo());
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 调用卡密激活
|
||||
String memberCode = tokenResult.get("memberCode");
|
||||
String token = tokenResult.get("token");
|
||||
JstActivateCardResult activateResult = jushitingApiService.activateCard(
|
||||
order.getDeviceId(), pool.getCardCode(), memberCode, token);
|
||||
|
||||
if (!activateResult.isSuccess()) {
|
||||
String errorMsg = activateResult.getErrorMessage() != null ? activateResult.getErrorMessage() : "卡密激活返回失败";
|
||||
updateActivateStatus(pool.getId(), DayCardConstants.ACTIVATE_STATUS_FAILED, errorMsg, null);
|
||||
logger.error("异步激活天卡失败:卡密激活返回失败, orderNo={}, cardCode={}", order.getOrderNo(), pool.getCardCode());
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. 确定卡有效期
|
||||
Date expireAt = null;
|
||||
// 优先使用激活接口返回的 expireTime
|
||||
if (StringUtils.isNotBlank(activateResult.getExpireTime())) {
|
||||
expireAt = parseExpireTime(activateResult.getExpireTime());
|
||||
}
|
||||
// 激活接口未返回到期时间,主动查询 VIP 到期时间
|
||||
if (expireAt == null) {
|
||||
String vipExpireTime = jushitingApiService.queryVipExpireTime(order.getDeviceId(), thirdCardRef);
|
||||
if (StringUtils.isNotBlank(vipExpireTime)) {
|
||||
expireAt = parseExpireTime(vipExpireTime);
|
||||
}
|
||||
}
|
||||
// 兜底:默认24小时有效期
|
||||
if (expireAt == null) {
|
||||
expireAt = new Date(System.currentTimeMillis() + 24 * 60 * 60 * 1000L);
|
||||
logger.warn("橘视厅未返回到期时间,使用默认24小时: orderNo={}", order.getOrderNo());
|
||||
}
|
||||
|
||||
// 4. 更新 day_card_pool:写入 expire_at + 激活成功状态
|
||||
UpdateWrapper<DayCardPoolEntity> poolUpdate = new UpdateWrapper<>();
|
||||
poolUpdate.eq("id", pool.getId())
|
||||
.set("expire_at", expireAt)
|
||||
.set("activate_status", DayCardConstants.ACTIVATE_STATUS_SUCCESS)
|
||||
.set("activated_at", new Date())
|
||||
.set("activate_error", null)
|
||||
.set("updated_at", new Date());
|
||||
dayCardPoolDao.update(null, poolUpdate);
|
||||
|
||||
logger.info("异步激活天卡完成: orderNo={}, cardCode={}, expireAt={}", order.getOrderNo(), pool.getCardCode(), expireAt);
|
||||
} catch (Exception e) {
|
||||
logger.error("异步激活天卡异常: orderNo={}", order.getOrderNo(), e);
|
||||
// 尝试更新激活失败状态
|
||||
try {
|
||||
QueryWrapper<DayCardPoolEntity> poolWrapper = new QueryWrapper<>();
|
||||
poolWrapper.eq("order_no", order.getOrderNo())
|
||||
.eq("delete_flag", DayCardConstants.DELETE_FLAG_UNDEL);
|
||||
DayCardPoolEntity pool = dayCardPoolDao.selectOne(poolWrapper);
|
||||
if (pool != null) {
|
||||
String errorMsg = e.getMessage();
|
||||
if (errorMsg != null && errorMsg.length() > 900) {
|
||||
errorMsg = errorMsg.substring(0, 900);
|
||||
}
|
||||
updateActivateStatus(pool.getId(), DayCardConstants.ACTIVATE_STATUS_FAILED, errorMsg, null);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
logger.error("更新激活失败状态异常: orderNo={}", order.getOrderNo(), ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新天卡激活状态
|
||||
*/
|
||||
private void updateActivateStatus(Long poolId, String status, String error, Date activatedAt) {
|
||||
UpdateWrapper<DayCardPoolEntity> update = new UpdateWrapper<>();
|
||||
update.eq("id", poolId)
|
||||
.set("activate_status", status)
|
||||
.set("activate_error", error)
|
||||
.set("updated_at", new Date());
|
||||
if (activatedAt != null) {
|
||||
update.set("activated_at", activatedAt);
|
||||
}
|
||||
dayCardPoolDao.update(null, update);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析橘视厅返回的到期时间字符串(格式如 "2027-04-24 16:48:58")
|
||||
*/
|
||||
private Date parseExpireTime(String timeStr) {
|
||||
if (timeStr == null || timeStr.trim().isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
|
||||
sdf.setTimeZone(java.util.TimeZone.getTimeZone("Asia/Shanghai"));
|
||||
return sdf.parse(timeStr.trim());
|
||||
} catch (Exception e) {
|
||||
logger.warn("解析到期时间失败: {}", timeStr);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public int cancelTimeoutOrders() {
|
||||
// 查询超时订单(状态CREATED, expire_time < now, LIMIT batch_size)
|
||||
QueryWrapper<DayCardOrderEntity> wrapper = new QueryWrapper<>();
|
||||
wrapper.eq("status", DayCardConstants.ORDER_STATUS_CREATED)
|
||||
.eq("delete_flag", DayCardConstants.DELETE_FLAG_UNDEL)
|
||||
.lt("expire_time", new Date())
|
||||
.last("LIMIT " + cancelBatchSize);
|
||||
List<DayCardOrderEntity> timeoutOrders = baseMapper.selectList(wrapper);
|
||||
|
||||
int cancelled = 0;
|
||||
for (DayCardOrderEntity order : timeoutOrders) {
|
||||
try {
|
||||
cancelSingleOrder(order);
|
||||
cancelled++;
|
||||
} catch (Exception e) {
|
||||
logger.error("取消超时订单失败: orderNo={}", order.getOrderNo(), e);
|
||||
}
|
||||
}
|
||||
|
||||
if (cancelled > 0) {
|
||||
logger.info("本次取消超时天卡订单 {} 笔", cancelled);
|
||||
}
|
||||
return cancelled;
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消单个超时订单:释放卡 + 更新订单CANCELLED
|
||||
*/
|
||||
private void cancelSingleOrder(DayCardOrderEntity order) {
|
||||
Date now = new Date();
|
||||
|
||||
// 微信支付订单需调用微信关单API
|
||||
if ("WECHAT".equals(order.getPayChannel())) {
|
||||
try {
|
||||
wechatPayService.closeOrder(order.getOrderNo());
|
||||
} catch (Exception e) {
|
||||
logger.warn("微信关单失败,继续本地取消: orderNo={}", order.getOrderNo(), e);
|
||||
}
|
||||
}
|
||||
|
||||
// 释放卡:RESERVED -> UNUSED
|
||||
UpdateWrapper<DayCardPoolEntity> poolUpdate = new UpdateWrapper<>();
|
||||
poolUpdate.eq("order_no", order.getOrderNo())
|
||||
.eq("status", DayCardConstants.POOL_STATUS_RESERVED)
|
||||
.eq("delete_flag", DayCardConstants.DELETE_FLAG_UNDEL)
|
||||
.set("status", DayCardConstants.POOL_STATUS_UNUSED)
|
||||
.set("order_no", null)
|
||||
.set("device_id", null)
|
||||
.set("updated_at", now);
|
||||
dayCardPoolDao.update(null, poolUpdate);
|
||||
|
||||
// 更新订单为CANCELLED
|
||||
UpdateWrapper<DayCardOrderEntity> orderUpdate = new UpdateWrapper<>();
|
||||
orderUpdate.eq("id", order.getId())
|
||||
.eq("status", DayCardConstants.ORDER_STATUS_CREATED)
|
||||
.set("status", DayCardConstants.ORDER_STATUS_CANCELLED)
|
||||
.set("cancel_time", now)
|
||||
.set("updated_at", now);
|
||||
baseMapper.update(null, orderUpdate);
|
||||
|
||||
// 清理Redis防重锁
|
||||
String lockKey = DayCardConstants.REDIS_DEVICE_ORDER_PREFIX + order.getDeviceId();
|
||||
redisUtils.delete(lockKey);
|
||||
// 兼容新下单接口(锁键使用mac/deviceSn/deviceUid)
|
||||
if (StringUtils.isNotBlank(order.getMac())) {
|
||||
redisUtils.delete(DayCardConstants.REDIS_DEVICE_ORDER_PREFIX + order.getMac());
|
||||
}
|
||||
if (StringUtils.isNotBlank(order.getDeviceSn())) {
|
||||
redisUtils.delete(DayCardConstants.REDIS_DEVICE_ORDER_PREFIX + order.getDeviceSn());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public DayCardOrderEntity getByOrderNo(String orderNo) {
|
||||
QueryWrapper<DayCardOrderEntity> wrapper = new QueryWrapper<>();
|
||||
wrapper.eq("order_no", orderNo)
|
||||
.eq("delete_flag", DayCardConstants.DELETE_FLAG_UNDEL);
|
||||
return baseMapper.selectOne(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DayCardPoolEntity getDeviceBenefits(Long deviceId) {
|
||||
QueryWrapper<DayCardPoolEntity> wrapper = new QueryWrapper<>();
|
||||
wrapper.eq("device_id", deviceId)
|
||||
.eq("status", DayCardConstants.POOL_STATUS_USED)
|
||||
.eq("delete_flag", DayCardConstants.DELETE_FLAG_UNDEL)
|
||||
.isNotNull("expire_at")
|
||||
.gt("expire_at", new Date())
|
||||
.orderByDesc("used_at")
|
||||
.last("LIMIT 1");
|
||||
return dayCardPoolDao.selectOne(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DayCardPriceConfigEntity getDevicePrice(Long deviceId) {
|
||||
DeviceOrgContext orgContext = deviceResolveService.resolveDevice(deviceId);
|
||||
DeviceEntity device = orgContext.getDevice();
|
||||
Long storeOrgId = device.getStoreOrgId();
|
||||
Long channelOrgId = orgContext.getChannelOrg() != null ? orgContext.getChannelOrg().getId() : null;
|
||||
return dayCardPriceConfigService.getEffectivePrice(channelOrgId, storeOrgId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unbindDevice(Long deviceId) {
|
||||
if (deviceId == null) {
|
||||
throw new ASException("设备ID不能为空");
|
||||
}
|
||||
DayCardPoolEntity pool = getDeviceBenefits(deviceId);
|
||||
if (pool == null) {
|
||||
throw new ASException("设备没有有效的天卡权益,无需解绑");
|
||||
}
|
||||
UpdateWrapper<DayCardPoolEntity> update = new UpdateWrapper<>();
|
||||
update.eq("id", pool.getId())
|
||||
.eq("status", DayCardConstants.POOL_STATUS_USED)
|
||||
.set("status", DayCardConstants.POOL_STATUS_REUSABLE)
|
||||
.set("device_id", null)
|
||||
.set("updated_at", new Date());
|
||||
int updated = dayCardPoolDao.update(null, update);
|
||||
if (updated == 0) {
|
||||
throw new ASException("解绑失败,卡状态可能已变更");
|
||||
}
|
||||
logger.info("设备解绑成功: deviceId={}, poolId={}, cardCode={}, expireAt={}",
|
||||
deviceId, pool.getId(), pool.getCardCode(), pool.getExpireAt());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unbindDeviceOrg(Long deviceId) {
|
||||
if (deviceId == null) {
|
||||
throw new ASException("设备ID不能为空");
|
||||
}
|
||||
Date now = new Date();
|
||||
// 1. 释放设备当前天卡权益(若有):USED -> REUSABLE,清卡上的 device_id
|
||||
DayCardPoolEntity pool = getDeviceBenefits(deviceId);
|
||||
if (pool != null) {
|
||||
UpdateWrapper<DayCardPoolEntity> poolUpdate = new UpdateWrapper<>();
|
||||
poolUpdate.eq("id", pool.getId())
|
||||
.eq("status", DayCardConstants.POOL_STATUS_USED)
|
||||
.set("status", DayCardConstants.POOL_STATUS_REUSABLE)
|
||||
.set("device_id", null)
|
||||
.set("updated_at", now);
|
||||
dayCardPoolDao.update(null, poolUpdate);
|
||||
logger.info("设备组织解绑-释放天卡权益: deviceId={}, poolId={}, cardCode={}",
|
||||
deviceId, pool.getId(), pool.getCardCode());
|
||||
}
|
||||
// 2. 清除设备的门店组织关联(storeOrgId),设备可在新场所用新激活码重新绑定
|
||||
UpdateWrapper<DeviceEntity> deviceUpdate = new UpdateWrapper<>();
|
||||
deviceUpdate.eq("id", deviceId)
|
||||
.eq("delete_flag", DayCardConstants.DELETE_FLAG_UNDEL)
|
||||
.set("store_org_id", null)
|
||||
.set("updated_at", now);
|
||||
int updated = deviceDao.update(null, deviceUpdate);
|
||||
if (updated == 0) {
|
||||
throw new ASException("设备不存在或解绑失败: " + deviceId);
|
||||
}
|
||||
// 3. 作废当前 sign,App 端旧 sign 立即失效,被迫回到激活码输入页(逆向 init)
|
||||
apiDeviceSignService.invalidate(deviceId);
|
||||
logger.info("设备组织解绑成功: deviceId={}", deviceId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int retryFailedActivations() {
|
||||
// 查询激活失败的卡(status=USED, activate_status=FAILED)
|
||||
QueryWrapper<DayCardPoolEntity> wrapper = new QueryWrapper<>();
|
||||
wrapper.eq("status", DayCardConstants.POOL_STATUS_USED)
|
||||
.eq("activate_status", DayCardConstants.ACTIVATE_STATUS_FAILED)
|
||||
.eq("delete_flag", DayCardConstants.DELETE_FLAG_UNDEL)
|
||||
.last("LIMIT " + cancelBatchSize);
|
||||
List<DayCardPoolEntity> failedPools = dayCardPoolDao.selectList(wrapper);
|
||||
|
||||
int retried = 0;
|
||||
for (DayCardPoolEntity pool : failedPools) {
|
||||
try {
|
||||
// 查询关联订单
|
||||
if (StringUtils.isBlank(pool.getOrderNo())) {
|
||||
continue;
|
||||
}
|
||||
DayCardOrderEntity order = getByOrderNo(pool.getOrderNo());
|
||||
if (order == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 重置激活状态为 PENDING
|
||||
updateActivateStatus(pool.getId(), DayCardConstants.ACTIVATE_STATUS_PENDING, null, null);
|
||||
|
||||
// 通过 selfProxy 调用确保 @Async 生效
|
||||
selfProxy.asyncActivateCard(order);
|
||||
retried++;
|
||||
} catch (Exception e) {
|
||||
logger.error("重试激活天卡失败: poolId={}, orderNo={}", pool.getId(), pool.getOrderNo(), e);
|
||||
}
|
||||
}
|
||||
|
||||
if (retried > 0) {
|
||||
logger.info("本次重试激活失败天卡 {} 张", retried);
|
||||
}
|
||||
return retried;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成订单号:yyyyMMddHHmmss + 6位随机数
|
||||
*/
|
||||
private String generateOrderNo() {
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
|
||||
String timestamp = sdf.format(new Date());
|
||||
int random = ThreadLocalRandom.current().nextInt(100000, 1000000);
|
||||
return timestamp + random;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
package io.as.modules.daycard.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import io.as.common.utils.PageUtils;
|
||||
import io.as.common.utils.Query;
|
||||
import io.as.modules.daycard.constants.DayCardConstants;
|
||||
import io.as.modules.daycard.dao.DayCardImportBatchDao;
|
||||
import io.as.modules.daycard.dao.DayCardPoolDao;
|
||||
import io.as.modules.daycard.entity.DayCardImportBatchEntity;
|
||||
import io.as.modules.daycard.entity.DayCardPoolEntity;
|
||||
import io.as.modules.daycard.service.DayCardPoolService;
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
|
||||
/**
|
||||
* 天卡池 Service 实现
|
||||
*/
|
||||
@Service("dayCardPoolService")
|
||||
public class DayCardPoolServiceImpl extends ServiceImpl<DayCardPoolDao, DayCardPoolEntity> implements DayCardPoolService {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(DayCardPoolServiceImpl.class);
|
||||
|
||||
private static final DateTimeFormatter CARD_NO_DATE = DateTimeFormatter.ofPattern("yyyyMMdd");
|
||||
private static final DateTimeFormatter THIRD_PARTY_REF_HHMMSS = DateTimeFormatter.ofPattern("HHmmss");
|
||||
private static final String THIRD_CARD_NO_ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
||||
private static final int THIRD_CARD_NO_LENGTH = 24;
|
||||
private static final String THIRD_CARD_NO_PREFIX = "D";
|
||||
private static final SecureRandom THIRD_CARD_NO_RANDOM = new SecureRandom();
|
||||
private static final int MAX_RETRY = 10;
|
||||
|
||||
@Autowired
|
||||
private DayCardImportBatchDao dayCardImportBatchDao;
|
||||
|
||||
@Override
|
||||
public PageUtils queryPage(Map<String, Object> params, Long partnerId) {
|
||||
IPage<DayCardPoolEntity> page = new Query<DayCardPoolEntity>().getPage(params, "created_at", false);
|
||||
|
||||
QueryWrapper<DayCardPoolEntity> wrapper = new QueryWrapper<>();
|
||||
wrapper.eq("delete_flag", DayCardConstants.DELETE_FLAG_UNDEL);
|
||||
if (partnerId != null) {
|
||||
wrapper.eq("partner_id", partnerId);
|
||||
}
|
||||
String status = (String) params.get("status");
|
||||
if (StringUtils.isNotBlank(status)) {
|
||||
wrapper.eq("status", status);
|
||||
}
|
||||
String cardCode = (String) params.get("cardCode");
|
||||
if (StringUtils.isNotBlank(cardCode)) {
|
||||
wrapper.eq("card_code", cardCode);
|
||||
}
|
||||
String orderNo = (String) params.get("orderNo");
|
||||
if (StringUtils.isNotBlank(orderNo)) {
|
||||
wrapper.eq("order_no", orderNo);
|
||||
}
|
||||
|
||||
IPage<DayCardPoolEntity> result = baseMapper.selectPage(page, wrapper);
|
||||
return new PageUtils(result);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getAvailableCount(Long partnerId) {
|
||||
QueryWrapper<DayCardPoolEntity> wrapper = new QueryWrapper<>();
|
||||
wrapper.eq("delete_flag", DayCardConstants.DELETE_FLAG_UNDEL)
|
||||
.eq("status", DayCardConstants.POOL_STATUS_UNUSED);
|
||||
if (partnerId != null) {
|
||||
wrapper.eq("partner_id", partnerId);
|
||||
}
|
||||
return baseMapper.selectCount(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public int importCards(List<String> cardCodes, Long partnerId) {
|
||||
if (cardCodes == null || cardCodes.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
Date now = new Date();
|
||||
String batchNo = "DC" + now.getTime();
|
||||
int successCount = 0;
|
||||
|
||||
// 创建导入批次
|
||||
DayCardImportBatchEntity batch = new DayCardImportBatchEntity();
|
||||
batch.setBatchNo(batchNo);
|
||||
batch.setPartnerId(partnerId);
|
||||
batch.setStatus(DayCardConstants.BATCH_STATUS_PROCESSING);
|
||||
batch.setTotalCount(cardCodes.size());
|
||||
batch.setSuccessCount(0);
|
||||
batch.setFailCount(0);
|
||||
batch.setDeleteFlag(DayCardConstants.DELETE_FLAG_UNDEL);
|
||||
batch.setCreatedAt(now);
|
||||
batch.setUpdatedAt(now);
|
||||
dayCardImportBatchDao.insert(batch);
|
||||
|
||||
Set<String> thirdCardNoReserved = new HashSet<>();
|
||||
List<DayCardPoolEntity> toInsert = new ArrayList<>();
|
||||
|
||||
for (String code : cardCodes) {
|
||||
String trimmed = code == null ? "" : code.trim();
|
||||
if (trimmed.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
// 查重:同一合作商下 cardCode 不重复
|
||||
long cnt = this.lambdaQuery()
|
||||
.eq(DayCardPoolEntity::getCardCode, trimmed)
|
||||
.eq(DayCardPoolEntity::getDeleteFlag, DayCardConstants.DELETE_FLAG_UNDEL)
|
||||
.count();
|
||||
if (cnt > 0) {
|
||||
logger.warn("导入天卡跳过重复卡密: cardCode={}", trimmed);
|
||||
continue;
|
||||
}
|
||||
|
||||
DayCardPoolEntity pool = new DayCardPoolEntity();
|
||||
pool.setCardCode(trimmed);
|
||||
pool.setThirdPartyRef(generateUniqueThirdPartyRef());
|
||||
pool.setThirdCardNo(generateThirdCardNo(thirdCardNoReserved));
|
||||
pool.setCardType(DayCardConstants.CARD_TYPE_DAY);
|
||||
pool.setPartnerId(partnerId);
|
||||
pool.setStatus(DayCardConstants.POOL_STATUS_UNUSED);
|
||||
pool.setDeleteFlag(DayCardConstants.DELETE_FLAG_UNDEL);
|
||||
pool.setCreatedAt(now);
|
||||
pool.setUpdatedAt(now);
|
||||
toInsert.add(pool);
|
||||
successCount++;
|
||||
}
|
||||
|
||||
if (!toInsert.isEmpty()) {
|
||||
// MyBatis-Plus saveBatch
|
||||
this.saveBatch(toInsert);
|
||||
}
|
||||
|
||||
// 更新批次状态
|
||||
batch.setStatus(DayCardConstants.BATCH_STATUS_SUCCEEDED);
|
||||
batch.setSuccessCount(successCount);
|
||||
batch.setFailCount(cardCodes.size() - successCount);
|
||||
batch.setUpdatedAt(new Date());
|
||||
dayCardImportBatchDao.updateById(batch);
|
||||
|
||||
logger.info("天卡导入完成: batchNo={}, total={}, success={}", batchNo, cardCodes.size(), successCount);
|
||||
return successCount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String generateUniqueThirdPartyRef() {
|
||||
ThreadLocalRandom rnd = ThreadLocalRandom.current();
|
||||
for (int attempt = 0; attempt < MAX_RETRY; attempt++) {
|
||||
LocalDateTime n = LocalDateTime.now();
|
||||
String ref = n.format(CARD_NO_DATE) + "-" + n.format(THIRD_PARTY_REF_HHMMSS) + "-"
|
||||
+ String.format("%04d", rnd.nextInt(10000));
|
||||
long cnt = this.lambdaQuery()
|
||||
.eq(DayCardPoolEntity::getThirdPartyRef, ref)
|
||||
.eq(DayCardPoolEntity::getDeleteFlag, DayCardConstants.DELETE_FLAG_UNDEL)
|
||||
.count();
|
||||
if (cnt == 0) {
|
||||
return ref;
|
||||
}
|
||||
}
|
||||
throw new RuntimeException("生成第三方引用号失败,请稍后重试");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String generateThirdCardNo() {
|
||||
return generateThirdCardNo(null);
|
||||
}
|
||||
|
||||
private String generateThirdCardNo(Set<String> inFlightReserved) {
|
||||
int alphaLen = THIRD_CARD_NO_ALPHABET.length();
|
||||
for (int attempt = 0; attempt < MAX_RETRY; attempt++) {
|
||||
StringBuilder sb = new StringBuilder(THIRD_CARD_NO_PREFIX.length() + THIRD_CARD_NO_LENGTH);
|
||||
sb.append(THIRD_CARD_NO_PREFIX);
|
||||
for (int i = 0; i < THIRD_CARD_NO_LENGTH; i++) {
|
||||
sb.append(THIRD_CARD_NO_ALPHABET.charAt(THIRD_CARD_NO_RANDOM.nextInt(alphaLen)));
|
||||
}
|
||||
String third = sb.toString();
|
||||
if (inFlightReserved != null && inFlightReserved.contains(third)) {
|
||||
continue;
|
||||
}
|
||||
long cnt = this.lambdaQuery()
|
||||
.eq(DayCardPoolEntity::getThirdCardNo, third)
|
||||
.eq(DayCardPoolEntity::getDeleteFlag, DayCardConstants.DELETE_FLAG_UNDEL)
|
||||
.count();
|
||||
if (cnt == 0) {
|
||||
if (inFlightReserved != null) {
|
||||
inFlightReserved.add(third);
|
||||
}
|
||||
return third;
|
||||
}
|
||||
}
|
||||
throw new RuntimeException("生成第三方卡号失败,请稍后重试");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package io.as.modules.daycard.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import io.as.common.utils.PageUtils;
|
||||
import io.as.common.utils.Query;
|
||||
import io.as.modules.daycard.constants.DayCardConstants;
|
||||
import io.as.modules.daycard.dao.DayCardPriceConfigDao;
|
||||
import io.as.modules.daycard.entity.DayCardPriceConfigEntity;
|
||||
import io.as.modules.daycard.service.DayCardPriceConfigService;
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 天卡定价策略 Service 实现
|
||||
*/
|
||||
@Service("dayCardPriceConfigService")
|
||||
public class DayCardPriceConfigServiceImpl extends ServiceImpl<DayCardPriceConfigDao, DayCardPriceConfigEntity> implements DayCardPriceConfigService {
|
||||
|
||||
@Override
|
||||
public PageUtils queryPage(Map<String, Object> params) {
|
||||
IPage<DayCardPriceConfigEntity> page = new Query<DayCardPriceConfigEntity>().getPage(params, "created_at", false);
|
||||
|
||||
QueryWrapper<DayCardPriceConfigEntity> wrapper = new QueryWrapper<>();
|
||||
wrapper.eq("delete_flag", DayCardConstants.DELETE_FLAG_UNDEL);
|
||||
|
||||
String partnerId = (String) params.get("partnerId");
|
||||
if (StringUtils.isNotBlank(partnerId)) {
|
||||
wrapper.eq("partner_id", partnerId);
|
||||
}
|
||||
String status = (String) params.get("status");
|
||||
if (StringUtils.isNotBlank(status)) {
|
||||
wrapper.eq("status", status);
|
||||
}
|
||||
String channelOrgId = (String) params.get("channelOrgId");
|
||||
if (StringUtils.isNotBlank(channelOrgId)) {
|
||||
wrapper.eq("channel_org_id", channelOrgId);
|
||||
}
|
||||
String storeOrgId = (String) params.get("storeOrgId");
|
||||
if (StringUtils.isNotBlank(storeOrgId)) {
|
||||
wrapper.eq("store_org_id", storeOrgId);
|
||||
}
|
||||
|
||||
IPage<DayCardPriceConfigEntity> result = baseMapper.selectPage(page, wrapper);
|
||||
return new PageUtils(result);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DayCardPriceConfigEntity getEffectivePrice(Long channelOrgId, Long storeOrgId) {
|
||||
// 1. 门店配置价(最优)
|
||||
if (storeOrgId != null) {
|
||||
QueryWrapper<DayCardPriceConfigEntity> storeWrapper = new QueryWrapper<>();
|
||||
storeWrapper.eq("store_org_id", storeOrgId)
|
||||
.eq("status", "ACTIVE")
|
||||
.eq("delete_flag", DayCardConstants.DELETE_FLAG_UNDEL)
|
||||
.last("LIMIT 1");
|
||||
DayCardPriceConfigEntity storePrice = baseMapper.selectOne(storeWrapper);
|
||||
if (storePrice != null) {
|
||||
return storePrice;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 渠道配置价(次优)
|
||||
if (channelOrgId != null) {
|
||||
QueryWrapper<DayCardPriceConfigEntity> channelWrapper = new QueryWrapper<>();
|
||||
channelWrapper.isNull("store_org_id")
|
||||
.eq("channel_org_id", channelOrgId)
|
||||
.eq("status", "ACTIVE")
|
||||
.eq("delete_flag", DayCardConstants.DELETE_FLAG_UNDEL)
|
||||
.last("LIMIT 1");
|
||||
DayCardPriceConfigEntity channelPrice = baseMapper.selectOne(channelWrapper);
|
||||
if (channelPrice != null) {
|
||||
return channelPrice;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 平台默认价兜底(store_org_id、channel_org_id 均为空)
|
||||
QueryWrapper<DayCardPriceConfigEntity> defaultWrapper = new QueryWrapper<>();
|
||||
defaultWrapper.isNull("store_org_id")
|
||||
.isNull("channel_org_id")
|
||||
.eq("status", "ACTIVE")
|
||||
.eq("delete_flag", DayCardConstants.DELETE_FLAG_UNDEL)
|
||||
.last("LIMIT 1");
|
||||
return baseMapper.selectOne(defaultWrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BigDecimal getPrice(Long channelOrgId, Long storeOrgId) {
|
||||
DayCardPriceConfigEntity config = getEffectivePrice(channelOrgId, storeOrgId);
|
||||
return config != null ? config.getPrice() : null;
|
||||
}
|
||||
}
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
package io.as.modules.daycard.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import io.as.common.utils.PageUtils;
|
||||
import io.as.common.utils.Query;
|
||||
import io.as.modules.daycard.constants.DayCardConstants;
|
||||
import io.as.modules.daycard.dao.DayCardProfitReceiverDao;
|
||||
import io.as.modules.daycard.entity.DayCardProfitReceiverEntity;
|
||||
import io.as.modules.daycard.service.DayCardProfitReceiverService;
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 天卡分账接收方 Service 实现
|
||||
*/
|
||||
@Service("dayCardProfitReceiverService")
|
||||
public class DayCardProfitReceiverServiceImpl extends ServiceImpl<DayCardProfitReceiverDao, DayCardProfitReceiverEntity> implements DayCardProfitReceiverService {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(DayCardProfitReceiverServiceImpl.class);
|
||||
|
||||
@Override
|
||||
public PageUtils queryPage(Map<String, Object> params) {
|
||||
IPage<DayCardProfitReceiverEntity> page = new Query<DayCardProfitReceiverEntity>().getPage(params, "created_at", false);
|
||||
|
||||
QueryWrapper<DayCardProfitReceiverEntity> wrapper = new QueryWrapper<>();
|
||||
wrapper.eq("delete_flag", DayCardConstants.DELETE_FLAG_UNDEL);
|
||||
|
||||
String partnerId = (String) params.get("partnerId");
|
||||
if (StringUtils.isNotBlank(partnerId)) {
|
||||
wrapper.eq("partner_id", partnerId);
|
||||
}
|
||||
String receiverType = (String) params.get("receiverType");
|
||||
if (StringUtils.isNotBlank(receiverType)) {
|
||||
wrapper.eq("receiver_type", receiverType);
|
||||
}
|
||||
String wxBindStatus = (String) params.get("wxBindStatus");
|
||||
if (StringUtils.isNotBlank(wxBindStatus)) {
|
||||
wrapper.eq("wx_bind_status", wxBindStatus);
|
||||
}
|
||||
|
||||
IPage<DayCardProfitReceiverEntity> result = baseMapper.selectPage(page, wrapper);
|
||||
return new PageUtils(result);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DayCardProfitReceiverEntity getById(Long id) {
|
||||
return baseMapper.selectById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveReceiver(DayCardProfitReceiverEntity receiver) {
|
||||
Date now = new Date();
|
||||
receiver.setDeleteFlag(DayCardConstants.DELETE_FLAG_UNDEL);
|
||||
if (receiver.getWxBindStatus() == null) {
|
||||
receiver.setWxBindStatus(DayCardConstants.WX_BIND_STATUS_UNBOUND);
|
||||
}
|
||||
receiver.setCreatedAt(now);
|
||||
receiver.setUpdatedAt(now);
|
||||
baseMapper.insert(receiver);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateReceiver(DayCardProfitReceiverEntity receiver) {
|
||||
receiver.setUpdatedAt(new Date());
|
||||
baseMapper.updateById(receiver);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(Long[] ids) {
|
||||
if (ids == null || ids.length == 0) {
|
||||
return;
|
||||
}
|
||||
List<DayCardProfitReceiverEntity> list = new ArrayList<>();
|
||||
Date now = new Date();
|
||||
for (Long id : ids) {
|
||||
DayCardProfitReceiverEntity entity = new DayCardProfitReceiverEntity();
|
||||
entity.setId(id);
|
||||
entity.setDeleteFlag(DayCardConstants.DELETE_FLAG_DEL);
|
||||
entity.setUpdatedAt(now);
|
||||
list.add(entity);
|
||||
}
|
||||
this.updateBatchById(list);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void syncToWechat(Long id) {
|
||||
DayCardProfitReceiverEntity receiver = baseMapper.selectById(id);
|
||||
if (receiver == null) {
|
||||
return;
|
||||
}
|
||||
// 占位方法:记录日志,设置绑定状态
|
||||
logger.info("同步分账接收方到微信: id={}, receiverAccount={}", id, receiver.getReceiverAccount());
|
||||
// TODO: 实际微信API待对接
|
||||
receiver.setWxBindStatus(DayCardConstants.WX_BIND_STATUS_BINDED);
|
||||
receiver.setUpdatedAt(new Date());
|
||||
baseMapper.updateById(receiver);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
package io.as.modules.daycard.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import io.as.common.exception.ASException;
|
||||
import io.as.common.utils.PageUtils;
|
||||
import io.as.common.utils.Query;
|
||||
import io.as.modules.daycard.constants.DayCardConstants;
|
||||
import io.as.modules.daycard.dao.DayCardProfitRuleDao;
|
||||
import io.as.modules.daycard.entity.DayCardProfitRuleEntity;
|
||||
import io.as.modules.daycard.service.DayCardProfitRuleService;
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 天卡分润规则 Service 实现
|
||||
*/
|
||||
@Service("dayCardProfitRuleService")
|
||||
public class DayCardProfitRuleServiceImpl extends ServiceImpl<DayCardProfitRuleDao, DayCardProfitRuleEntity> implements DayCardProfitRuleService {
|
||||
|
||||
@Override
|
||||
public PageUtils queryPage(Map<String, Object> params) {
|
||||
IPage<DayCardProfitRuleEntity> page = new Query<DayCardProfitRuleEntity>().getPage(params, "created_at", false);
|
||||
|
||||
QueryWrapper<DayCardProfitRuleEntity> wrapper = new QueryWrapper<>();
|
||||
wrapper.eq("delete_flag", DayCardConstants.DELETE_FLAG_UNDEL);
|
||||
|
||||
String partnerId = (String) params.get("partnerId");
|
||||
if (StringUtils.isNotBlank(partnerId)) {
|
||||
wrapper.eq("partner_id", partnerId);
|
||||
}
|
||||
String channelOrgId = (String) params.get("channelOrgId");
|
||||
if (StringUtils.isNotBlank(channelOrgId)) {
|
||||
wrapper.eq("channel_org_id", channelOrgId);
|
||||
}
|
||||
String storeOrgId = (String) params.get("storeOrgId");
|
||||
if (StringUtils.isNotBlank(storeOrgId)) {
|
||||
wrapper.eq("store_org_id", storeOrgId);
|
||||
}
|
||||
String status = (String) params.get("status");
|
||||
if (StringUtils.isNotBlank(status)) {
|
||||
wrapper.eq("status", status);
|
||||
}
|
||||
|
||||
IPage<DayCardProfitRuleEntity> result = baseMapper.selectPage(page, wrapper);
|
||||
return new PageUtils(result);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DayCardProfitRuleEntity getById(Long id) {
|
||||
return baseMapper.selectById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveRule(DayCardProfitRuleEntity rule) {
|
||||
validateShareRatio(rule);
|
||||
Date now = new Date();
|
||||
rule.setDeleteFlag(DayCardConstants.DELETE_FLAG_UNDEL);
|
||||
rule.setCreatedAt(now);
|
||||
rule.setUpdatedAt(now);
|
||||
baseMapper.insert(rule);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateRule(DayCardProfitRuleEntity rule) {
|
||||
validateShareRatio(rule);
|
||||
rule.setUpdatedAt(new Date());
|
||||
baseMapper.updateById(rule);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(Long[] ids) {
|
||||
if (ids == null || ids.length == 0) {
|
||||
return;
|
||||
}
|
||||
List<DayCardProfitRuleEntity> list = new ArrayList<>();
|
||||
Date now = new Date();
|
||||
for (Long id : ids) {
|
||||
DayCardProfitRuleEntity entity = new DayCardProfitRuleEntity();
|
||||
entity.setId(id);
|
||||
entity.setDeleteFlag(DayCardConstants.DELETE_FLAG_DEL);
|
||||
entity.setUpdatedAt(now);
|
||||
list.add(entity);
|
||||
}
|
||||
this.updateBatchById(list);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<DayCardProfitRuleEntity> matchRules(Long partnerId, Long channelOrgId, Long storeOrgId) {
|
||||
// 1. 优先查门店级匹配的规则
|
||||
if (storeOrgId != null && channelOrgId != null) {
|
||||
QueryWrapper<DayCardProfitRuleEntity> storeWrapper = new QueryWrapper<>();
|
||||
storeWrapper.eq("partner_id", partnerId)
|
||||
.eq("channel_org_id", channelOrgId)
|
||||
.eq("store_org_id", storeOrgId)
|
||||
.eq("status", DayCardConstants.PROFIT_RULE_STATUS_ACTIVE)
|
||||
.eq("delete_flag", DayCardConstants.DELETE_FLAG_UNDEL)
|
||||
.orderByDesc("priority");
|
||||
List<DayCardProfitRuleEntity> storeRules = baseMapper.selectList(storeWrapper);
|
||||
if (!storeRules.isEmpty()) {
|
||||
return storeRules;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 无则查渠道级匹配的规则(store_org_id 为 null)
|
||||
if (channelOrgId != null) {
|
||||
QueryWrapper<DayCardProfitRuleEntity> channelWrapper = new QueryWrapper<>();
|
||||
channelWrapper.eq("partner_id", partnerId)
|
||||
.eq("channel_org_id", channelOrgId)
|
||||
.isNull("store_org_id")
|
||||
.eq("status", DayCardConstants.PROFIT_RULE_STATUS_ACTIVE)
|
||||
.eq("delete_flag", DayCardConstants.DELETE_FLAG_UNDEL)
|
||||
.orderByDesc("priority");
|
||||
List<DayCardProfitRuleEntity> channelRules = baseMapper.selectList(channelWrapper);
|
||||
if (!channelRules.isEmpty()) {
|
||||
return channelRules;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 无则查合作商级匹配的规则(channel_org_id 和 store_org_id 都为 null)
|
||||
QueryWrapper<DayCardProfitRuleEntity> partnerWrapper = new QueryWrapper<>();
|
||||
partnerWrapper.eq("partner_id", partnerId)
|
||||
.isNull("channel_org_id")
|
||||
.isNull("store_org_id")
|
||||
.eq("status", DayCardConstants.PROFIT_RULE_STATUS_ACTIVE)
|
||||
.eq("delete_flag", DayCardConstants.DELETE_FLAG_UNDEL)
|
||||
.orderByDesc("priority");
|
||||
return baseMapper.selectList(partnerWrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验分润比例不超过 30%
|
||||
*/
|
||||
private void validateShareRatio(DayCardProfitRuleEntity rule) {
|
||||
if (rule.getShareRatio() == null) {
|
||||
throw new ASException("分润比例不能为空");
|
||||
}
|
||||
if (DayCardConstants.SHARE_TYPE_FIXED.equals(rule.getShareType())) {
|
||||
// 固定金额类型,不校验比例
|
||||
return;
|
||||
}
|
||||
if (rule.getShareRatio().compareTo(DayCardConstants.SHARE_RATIO_LIMIT) > 0) {
|
||||
throw new ASException("分润比例不能超过30%");
|
||||
}
|
||||
if (rule.getShareRatio().compareTo(BigDecimal.ZERO) <= 0) {
|
||||
throw new ASException("分润比例必须大于0");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,301 @@
|
||||
package io.as.modules.daycard.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import io.as.common.exception.ASException;
|
||||
import io.as.common.utils.PageUtils;
|
||||
import io.as.common.utils.Query;
|
||||
import io.as.modules.daycard.constants.DayCardConstants;
|
||||
import io.as.modules.daycard.dao.DayCardOrderDao;
|
||||
import io.as.modules.daycard.dao.DayCardPoolDao;
|
||||
import io.as.modules.daycard.dao.DayCardRefundDao;
|
||||
import io.as.modules.daycard.entity.DayCardOrderEntity;
|
||||
import io.as.modules.daycard.entity.DayCardPoolEntity;
|
||||
import io.as.modules.daycard.entity.DayCardRefundEntity;
|
||||
import io.as.modules.daycard.entity.ProfitSharingDetailEntity;
|
||||
import io.as.modules.daycard.entity.ProfitSharingOrderEntity;
|
||||
import io.as.modules.daycard.service.DayCardRefundService;
|
||||
import io.as.modules.payment.service.WechatPayResult;
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
|
||||
/**
|
||||
* 天卡退款 Service 实现
|
||||
*/
|
||||
@Service("dayCardRefundService")
|
||||
public class DayCardRefundServiceImpl extends ServiceImpl<DayCardRefundDao, DayCardRefundEntity> implements DayCardRefundService {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(DayCardRefundServiceImpl.class);
|
||||
|
||||
@Autowired
|
||||
private DayCardOrderDao dayCardOrderDao;
|
||||
@Autowired
|
||||
private DayCardPoolDao dayCardPoolDao;
|
||||
|
||||
@Autowired
|
||||
private io.as.modules.daycard.dao.ProfitSharingOrderDao profitSharingOrderDao;
|
||||
@Autowired
|
||||
private io.as.modules.daycard.dao.ProfitSharingDetailDao profitSharingDetailDao;
|
||||
@Autowired
|
||||
@org.springframework.context.annotation.Lazy
|
||||
private io.as.modules.payment.service.WechatPayService wechatPayService;
|
||||
|
||||
@Override
|
||||
public PageUtils queryPage(Map<String, Object> params) {
|
||||
IPage<DayCardRefundEntity> page = new Query<DayCardRefundEntity>().getPage(params, "created_at", false);
|
||||
|
||||
QueryWrapper<DayCardRefundEntity> wrapper = new QueryWrapper<>();
|
||||
wrapper.eq("delete_flag", DayCardConstants.DELETE_FLAG_UNDEL);
|
||||
|
||||
String refundNo = (String) params.get("refundNo");
|
||||
if (StringUtils.isNotBlank(refundNo)) {
|
||||
wrapper.eq("refund_no", refundNo);
|
||||
}
|
||||
String orderNo = (String) params.get("orderNo");
|
||||
if (StringUtils.isNotBlank(orderNo)) {
|
||||
wrapper.eq("order_no", orderNo);
|
||||
}
|
||||
String status = (String) params.get("status");
|
||||
if (StringUtils.isNotBlank(status)) {
|
||||
wrapper.eq("status", status);
|
||||
}
|
||||
|
||||
IPage<DayCardRefundEntity> result = baseMapper.selectPage(page, wrapper);
|
||||
return new PageUtils(result);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DayCardRefundEntity getByRefundNo(String refundNo) {
|
||||
QueryWrapper<DayCardRefundEntity> wrapper = new QueryWrapper<>();
|
||||
wrapper.eq("refund_no", refundNo)
|
||||
.eq("delete_flag", DayCardConstants.DELETE_FLAG_UNDEL);
|
||||
return baseMapper.selectOne(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public DayCardRefundEntity refund(String orderNo, String reason) {
|
||||
if (StringUtils.isBlank(orderNo)) {
|
||||
throw new ASException("订单号不能为空");
|
||||
}
|
||||
|
||||
Date now = new Date();
|
||||
|
||||
// 1. 查询订单,校验 status=PAID
|
||||
QueryWrapper<DayCardOrderEntity> orderWrapper = new QueryWrapper<>();
|
||||
orderWrapper.eq("order_no", orderNo)
|
||||
.eq("delete_flag", DayCardConstants.DELETE_FLAG_UNDEL);
|
||||
DayCardOrderEntity order = dayCardOrderDao.selectOne(orderWrapper);
|
||||
if (order == null) {
|
||||
throw new ASException("订单不存在: " + orderNo);
|
||||
}
|
||||
if (!DayCardConstants.ORDER_STATUS_PAID.equals(order.getStatus())) {
|
||||
throw new ASException("订单状态不允许退款,当前状态: " + order.getStatus());
|
||||
}
|
||||
|
||||
// 2. 退款金额:优先取 payAmount,为空则用 amount
|
||||
BigDecimal refundAmount = order.getPayAmount() != null ? order.getPayAmount() : order.getAmount();
|
||||
if (refundAmount == null || refundAmount.compareTo(BigDecimal.ZERO) <= 0) {
|
||||
throw new ASException("退款金额无效");
|
||||
}
|
||||
|
||||
// 3. 生成退款单号(REFUND + yyyyMMddHHmmss + 6位随机)
|
||||
String refundNo = generateRefundNo();
|
||||
|
||||
// 4. 创建退款记录 status=PENDING
|
||||
DayCardRefundEntity refund = new DayCardRefundEntity();
|
||||
refund.setRefundNo(refundNo);
|
||||
refund.setOrderNo(orderNo);
|
||||
refund.setRefundAmount(refundAmount);
|
||||
refund.setReason(reason);
|
||||
refund.setStatus(DayCardConstants.REFUND_STATUS_PENDING);
|
||||
refund.setDeleteFlag(DayCardConstants.DELETE_FLAG_UNDEL);
|
||||
refund.setCreatedAt(now);
|
||||
refund.setUpdatedAt(now);
|
||||
baseMapper.insert(refund);
|
||||
|
||||
try {
|
||||
// 5. 更新订单状态为 REFUNDING
|
||||
UpdateWrapper<DayCardOrderEntity> orderUpdate = new UpdateWrapper<>();
|
||||
orderUpdate.eq("id", order.getId())
|
||||
.eq("status", DayCardConstants.ORDER_STATUS_PAID)
|
||||
.set("status", DayCardConstants.ORDER_STATUS_REFUNDING)
|
||||
.set("updated_at", now);
|
||||
int updated = dayCardOrderDao.update(null, orderUpdate);
|
||||
if (updated == 0) {
|
||||
throw new ASException("订单状态已变更,退款失败");
|
||||
}
|
||||
|
||||
// 6. 释放卡:USED -> UNUSED,清除 device_id, order_no, expire_at, activate_status
|
||||
UpdateWrapper<DayCardPoolEntity> poolUpdate = new UpdateWrapper<>();
|
||||
poolUpdate.eq("order_no", orderNo)
|
||||
.eq("status", DayCardConstants.POOL_STATUS_USED)
|
||||
.eq("delete_flag", DayCardConstants.DELETE_FLAG_UNDEL)
|
||||
.set("status", DayCardConstants.POOL_STATUS_UNUSED)
|
||||
.set("order_no", null)
|
||||
.set("device_id", null)
|
||||
.set("expire_at", null)
|
||||
.set("activate_status", null)
|
||||
.set("updated_at", now);
|
||||
dayCardPoolDao.update(null, poolUpdate);
|
||||
logger.info("退款释放卡成功: orderNo={}", orderNo);
|
||||
|
||||
// 7. 尝试回退分账(设置关联分账单状态为 CLOSED)
|
||||
rollbackSettleRecords(orderNo, now);
|
||||
|
||||
// 8. 调用微信退款(Mock支付则跳过)
|
||||
String payNo = order.getPayNo();
|
||||
if (isWechatPayment(payNo)) {
|
||||
BigDecimal totalAmount = order.getAmount();
|
||||
WechatPayResult refundResult = wechatPayService.refund(orderNo, refundNo, refundAmount, totalAmount, reason);
|
||||
if (!refundResult.isSuccess()) {
|
||||
throw new ASException("微信退款失败: " + refundResult.getErrorMsg());
|
||||
}
|
||||
logger.info("微信退款成功: orderNo={}, refundNo={}", orderNo, refundNo);
|
||||
} else {
|
||||
logger.info("Mock支付退款,无需调用微信接口: orderNo={}, refundNo={}", orderNo, refundNo);
|
||||
}
|
||||
|
||||
// 9. 更新退款记录 status=SUCCESS
|
||||
refund.setStatus(DayCardConstants.REFUND_STATUS_SUCCESS);
|
||||
refund.setSettledAt(new Date());
|
||||
refund.setUpdatedAt(new Date());
|
||||
baseMapper.updateById(refund);
|
||||
|
||||
logger.info("退款成功: orderNo={}, refundNo={}, refundAmount={}", orderNo, refundNo, refundAmount);
|
||||
return refund;
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("退款处理失败: orderNo={}, refundNo={}", orderNo, refundNo, e);
|
||||
// 更新退款记录为 FAILED
|
||||
String errorMsg = e.getMessage();
|
||||
if (errorMsg != null && errorMsg.length() > 450) {
|
||||
errorMsg = errorMsg.substring(0, 450);
|
||||
}
|
||||
refund.setStatus(DayCardConstants.REFUND_STATUS_FAILED);
|
||||
refund.setErrorMessage(errorMsg);
|
||||
refund.setUpdatedAt(new Date());
|
||||
baseMapper.updateById(refund);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateStatus(Long id, String status, String errorMessage) {
|
||||
UpdateWrapper<DayCardRefundEntity> update = new UpdateWrapper<>();
|
||||
update.eq("id", id)
|
||||
.set("status", status)
|
||||
.set("error_message", errorMessage)
|
||||
.set("updated_at", new Date());
|
||||
if (DayCardConstants.REFUND_STATUS_SUCCESS.equals(status)
|
||||
|| DayCardConstants.REFUND_STATUS_CLOSED.equals(status)) {
|
||||
update.set("settled_at", new Date());
|
||||
}
|
||||
baseMapper.update(null, update);
|
||||
}
|
||||
|
||||
/**
|
||||
* 回退分账:查询订单关联的分账单,逐方回退已分账金额
|
||||
*/
|
||||
private void rollbackSettleRecords(String orderNo, Date now) {
|
||||
try {
|
||||
// 查询关联的分账单
|
||||
QueryWrapper<ProfitSharingOrderEntity> orderWrapper = new QueryWrapper<>();
|
||||
orderWrapper.eq("pay_order_no", orderNo)
|
||||
.eq("delete_flag", DayCardConstants.DELETE_FLAG_UNDEL);
|
||||
ProfitSharingOrderEntity sharingOrder = profitSharingOrderDao.selectOne(orderWrapper);
|
||||
if (sharingOrder == null) {
|
||||
logger.info("无关联分账单,跳过分账回退: orderNo={}", orderNo);
|
||||
return;
|
||||
}
|
||||
|
||||
// 查询分账明细
|
||||
QueryWrapper<ProfitSharingDetailEntity> detailWrapper = new QueryWrapper<>();
|
||||
detailWrapper.eq("sharing_order_id", sharingOrder.getId());
|
||||
List<ProfitSharingDetailEntity> details = profitSharingDetailDao.selectList(detailWrapper);
|
||||
if (details == null || details.isEmpty()) {
|
||||
logger.info("无分账明细,跳过分账回退: orderNo={}", orderNo);
|
||||
return;
|
||||
}
|
||||
|
||||
// 逐方回退已成功的分账
|
||||
for (ProfitSharingDetailEntity detail : details) {
|
||||
if (!DayCardConstants.SETTLE_STATUS_SUCCESS.equals(detail.getStatus())) {
|
||||
continue;
|
||||
}
|
||||
if (detail.getReceiverAccount() == null || detail.getAmount() == null) {
|
||||
continue;
|
||||
}
|
||||
// 调用微信分账回退 API
|
||||
String outReturnNo = "RT" + System.currentTimeMillis() + ThreadLocalRandom.current().nextInt(1000, 9999);
|
||||
WechatPayResult returnResult = wechatPayService.profitSharingReturn(
|
||||
sharingOrder.getOutOrderNo(),
|
||||
detail.getReceiverAccount(),
|
||||
detail.getAmount(),
|
||||
outReturnNo);
|
||||
|
||||
// 更新明细状态
|
||||
UpdateWrapper<ProfitSharingDetailEntity> detailUpdate = new UpdateWrapper<>();
|
||||
detailUpdate.eq("id", detail.getId());
|
||||
if (returnResult.isSuccess()) {
|
||||
detailUpdate.set("status", DayCardConstants.REFUND_STATUS_CLOSED)
|
||||
.set("updated_at", now);
|
||||
} else {
|
||||
detailUpdate.set("fail_reason", returnResult.getErrorMsg())
|
||||
.set("updated_at", now);
|
||||
}
|
||||
profitSharingDetailDao.update(null, detailUpdate);
|
||||
}
|
||||
|
||||
// 更新分账单状态为 CLOSED
|
||||
UpdateWrapper<ProfitSharingOrderEntity> orderUpdate = new UpdateWrapper<>();
|
||||
orderUpdate.eq("id", sharingOrder.getId())
|
||||
.set("status", DayCardConstants.REFUND_STATUS_CLOSED)
|
||||
.set("updated_at", now);
|
||||
profitSharingOrderDao.update(null, orderUpdate);
|
||||
|
||||
logger.info("分账回退完成: orderNo={}", orderNo);
|
||||
} catch (Exception e) {
|
||||
logger.warn("分账回退失败,跳过: orderNo={}", orderNo, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为微信支付
|
||||
* <p>Mock 支付的 payNo 以 "MOCK_" 开头,其余视为微信支付</p>
|
||||
*
|
||||
* @param payNo 支付单号
|
||||
* @return true=微信支付
|
||||
*/
|
||||
private boolean isWechatPayment(String payNo) {
|
||||
if (StringUtils.isBlank(payNo)) {
|
||||
// 没有支付单号,默认视为微信支付
|
||||
return true;
|
||||
}
|
||||
return !payNo.startsWith(DayCardConstants.PAY_CHANNEL_MOCK + "_");
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成退款单号:REFUND + yyyyMMddHHmmss + 6位随机数
|
||||
*/
|
||||
private String generateRefundNo() {
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
|
||||
String timestamp = sdf.format(new Date());
|
||||
int random = ThreadLocalRandom.current().nextInt(100000, 1000000);
|
||||
return "REFUND" + timestamp + random;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package io.as.modules.daycard.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import io.as.common.utils.PageUtils;
|
||||
import io.as.common.utils.Query;
|
||||
import io.as.modules.daycard.constants.DayCardConstants;
|
||||
import io.as.modules.daycard.dao.DayCardSettleOrderDao;
|
||||
import io.as.modules.daycard.entity.DayCardSettleOrderEntity;
|
||||
import io.as.modules.daycard.service.DayCardSettleOrderService;
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 天卡分账单 Service 实现
|
||||
*/
|
||||
@Service("dayCardSettleOrderService")
|
||||
public class DayCardSettleOrderServiceImpl extends ServiceImpl<DayCardSettleOrderDao, DayCardSettleOrderEntity> implements DayCardSettleOrderService {
|
||||
|
||||
@Override
|
||||
public PageUtils queryPage(Map<String, Object> params) {
|
||||
IPage<DayCardSettleOrderEntity> page = new Query<DayCardSettleOrderEntity>().getPage(params, "created_at", false);
|
||||
|
||||
QueryWrapper<DayCardSettleOrderEntity> wrapper = new QueryWrapper<>();
|
||||
|
||||
String orderNo = (String) params.get("orderNo");
|
||||
if (StringUtils.isNotBlank(orderNo)) {
|
||||
wrapper.eq("order_no", orderNo);
|
||||
}
|
||||
String status = (String) params.get("status");
|
||||
if (StringUtils.isNotBlank(status)) {
|
||||
wrapper.eq("status", status);
|
||||
}
|
||||
|
||||
IPage<DayCardSettleOrderEntity> result = baseMapper.selectPage(page, wrapper);
|
||||
return new PageUtils(result);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DayCardSettleOrderEntity getBySettleNo(String settleNo) {
|
||||
QueryWrapper<DayCardSettleOrderEntity> wrapper = new QueryWrapper<>();
|
||||
wrapper.eq("settle_no", settleNo);
|
||||
return baseMapper.selectOne(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveSettleOrder(DayCardSettleOrderEntity entity) {
|
||||
Date now = new Date();
|
||||
entity.setCreatedAt(now);
|
||||
entity.setUpdatedAt(now);
|
||||
baseMapper.insert(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateStatus(Long id, String status, String errorMessage) {
|
||||
UpdateWrapper<DayCardSettleOrderEntity> update = new UpdateWrapper<>();
|
||||
update.eq("id", id)
|
||||
.set("status", status)
|
||||
.set("error_message", errorMessage)
|
||||
.set("updated_at", new Date());
|
||||
if (DayCardConstants.SETTLE_STATUS_SUCCESS.equals(status)) {
|
||||
update.set("settled_at", new Date());
|
||||
}
|
||||
baseMapper.update(null, update);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<DayCardSettleOrderEntity> getByOrderNo(String orderNo) {
|
||||
QueryWrapper<DayCardSettleOrderEntity> wrapper = new QueryWrapper<>();
|
||||
wrapper.eq("order_no", orderNo)
|
||||
.orderByDesc("created_at");
|
||||
return baseMapper.selectList(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int retryFailed() {
|
||||
QueryWrapper<DayCardSettleOrderEntity> wrapper = new QueryWrapper<>();
|
||||
wrapper.eq("status", DayCardConstants.SETTLE_STATUS_FAILED)
|
||||
.last("LIMIT 200");
|
||||
List<DayCardSettleOrderEntity> failedList = baseMapper.selectList(wrapper);
|
||||
|
||||
int retried = 0;
|
||||
for (DayCardSettleOrderEntity settleOrder : failedList) {
|
||||
try {
|
||||
// 重置状态为 PENDING,等待重新执行
|
||||
updateStatus(settleOrder.getId(), DayCardConstants.SETTLE_STATUS_PENDING, null);
|
||||
retried++;
|
||||
} catch (Exception e) {
|
||||
// 忽略单条重置失败
|
||||
}
|
||||
}
|
||||
return retried;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package io.as.modules.daycard.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import io.as.common.utils.PageUtils;
|
||||
import io.as.common.utils.Query;
|
||||
import io.as.modules.daycard.dao.DayCardSettleRecordDao;
|
||||
import io.as.modules.daycard.entity.DayCardSettleRecordEntity;
|
||||
import io.as.modules.daycard.service.DayCardSettleRecordService;
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 天卡分账记录 Service 实现
|
||||
*/
|
||||
@Service("dayCardSettleRecordService")
|
||||
public class DayCardSettleRecordServiceImpl extends ServiceImpl<DayCardSettleRecordDao, DayCardSettleRecordEntity> implements DayCardSettleRecordService {
|
||||
|
||||
@Override
|
||||
public PageUtils queryPage(Map<String, Object> params) {
|
||||
IPage<DayCardSettleRecordEntity> page = new Query<DayCardSettleRecordEntity>().getPage(params, "created_at", false);
|
||||
|
||||
QueryWrapper<DayCardSettleRecordEntity> wrapper = new QueryWrapper<>();
|
||||
|
||||
String orderNo = (String) params.get("orderNo");
|
||||
if (StringUtils.isNotBlank(orderNo)) {
|
||||
wrapper.eq("order_no", orderNo);
|
||||
}
|
||||
String status = (String) params.get("status");
|
||||
if (StringUtils.isNotBlank(status)) {
|
||||
wrapper.eq("status", status);
|
||||
}
|
||||
|
||||
IPage<DayCardSettleRecordEntity> result = baseMapper.selectPage(page, wrapper);
|
||||
return new PageUtils(result);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DayCardSettleRecordEntity getById(Long id) {
|
||||
return baseMapper.selectById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveRecord(DayCardSettleRecordEntity record) {
|
||||
Date now = new Date();
|
||||
record.setCreatedAt(now);
|
||||
record.setUpdatedAt(now);
|
||||
baseMapper.insert(record);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
package io.as.modules.daycard.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import io.as.common.utils.PageUtils;
|
||||
import io.as.common.utils.Query;
|
||||
import io.as.modules.daycard.constants.DayCardConstants;
|
||||
import io.as.modules.daycard.dao.DayCardSettleRuleDao;
|
||||
import io.as.modules.daycard.entity.DayCardSettleRuleEntity;
|
||||
import io.as.modules.daycard.service.DayCardSettleRuleService;
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 天卡分账规则 Service 实现
|
||||
*/
|
||||
@Service("dayCardSettleRuleService")
|
||||
public class DayCardSettleRuleServiceImpl extends ServiceImpl<DayCardSettleRuleDao, DayCardSettleRuleEntity> implements DayCardSettleRuleService {
|
||||
|
||||
@Override
|
||||
public PageUtils queryPage(Map<String, Object> params) {
|
||||
IPage<DayCardSettleRuleEntity> page = new Query<DayCardSettleRuleEntity>().getPage(params, "created_at", false);
|
||||
|
||||
QueryWrapper<DayCardSettleRuleEntity> wrapper = new QueryWrapper<>();
|
||||
wrapper.eq("delete_flag", DayCardConstants.DELETE_FLAG_UNDEL);
|
||||
|
||||
String partnerId = (String) params.get("partnerId");
|
||||
if (StringUtils.isNotBlank(partnerId)) {
|
||||
wrapper.eq("partner_id", partnerId);
|
||||
}
|
||||
String channelOrgId = (String) params.get("channelOrgId");
|
||||
if (StringUtils.isNotBlank(channelOrgId)) {
|
||||
wrapper.eq("channel_org_id", channelOrgId);
|
||||
}
|
||||
String status = (String) params.get("status");
|
||||
if (StringUtils.isNotBlank(status)) {
|
||||
wrapper.eq("status", status);
|
||||
}
|
||||
|
||||
IPage<DayCardSettleRuleEntity> result = baseMapper.selectPage(page, wrapper);
|
||||
return new PageUtils(result);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DayCardSettleRuleEntity getById(Long id) {
|
||||
return baseMapper.selectById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveRule(DayCardSettleRuleEntity rule) {
|
||||
Date now = new Date();
|
||||
rule.setDeleteFlag(DayCardConstants.DELETE_FLAG_UNDEL);
|
||||
rule.setCreatedAt(now);
|
||||
rule.setUpdatedAt(now);
|
||||
baseMapper.insert(rule);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(DayCardSettleRuleEntity rule) {
|
||||
rule.setUpdatedAt(new Date());
|
||||
baseMapper.updateById(rule);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(Long[] ids) {
|
||||
if (ids == null || ids.length == 0) {
|
||||
return;
|
||||
}
|
||||
List<DayCardSettleRuleEntity> list = new ArrayList<>();
|
||||
Date now = new Date();
|
||||
for (Long id : ids) {
|
||||
DayCardSettleRuleEntity entity = new DayCardSettleRuleEntity();
|
||||
entity.setId(id);
|
||||
entity.setDeleteFlag(DayCardConstants.DELETE_FLAG_DEL);
|
||||
entity.setUpdatedAt(now);
|
||||
list.add(entity);
|
||||
}
|
||||
this.updateBatchById(list);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<DayCardSettleRuleEntity> getRulesByOrder(Long partnerId, Long channelOrgId, Long storeOrgId) {
|
||||
QueryWrapper<DayCardSettleRuleEntity> wrapper = new QueryWrapper<>();
|
||||
wrapper.eq("partner_id", partnerId)
|
||||
.eq("status", "ACTIVE")
|
||||
.eq("delete_flag", DayCardConstants.DELETE_FLAG_UNDEL);
|
||||
|
||||
// 门店级规则(store_org_id 不为空且匹配)
|
||||
if (storeOrgId != null) {
|
||||
QueryWrapper<DayCardSettleRuleEntity> storeWrapper = new QueryWrapper<>();
|
||||
storeWrapper.eq("partner_id", partnerId)
|
||||
.eq("channel_org_id", channelOrgId)
|
||||
.eq("store_org_id", storeOrgId)
|
||||
.eq("status", "ACTIVE")
|
||||
.eq("delete_flag", DayCardConstants.DELETE_FLAG_UNDEL);
|
||||
List<DayCardSettleRuleEntity> storeRules = baseMapper.selectList(storeWrapper);
|
||||
if (!storeRules.isEmpty()) {
|
||||
return storeRules;
|
||||
}
|
||||
}
|
||||
|
||||
// 渠道级规则(store_org_id 为空)
|
||||
wrapper.eq("channel_org_id", channelOrgId)
|
||||
.isNull("store_org_id");
|
||||
return baseMapper.selectList(wrapper);
|
||||
}
|
||||
}
|
||||
+188
@@ -0,0 +1,188 @@
|
||||
package io.as.modules.daycard.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import io.as.common.utils.PageUtils;
|
||||
import io.as.common.utils.Query;
|
||||
import io.as.modules.daycard.constants.DayCardConstants;
|
||||
import io.as.modules.daycard.dao.OrgSettlementAccountDao;
|
||||
import io.as.modules.daycard.entity.OrgSettlementAccountEntity;
|
||||
import io.as.modules.daycard.service.OrgSettlementAccountService;
|
||||
import io.as.modules.payment.service.WechatPayResult;
|
||||
import io.as.modules.payment.service.WechatPayService;
|
||||
import io.as.modules.sys.dao.SysOrgDao;
|
||||
import io.as.modules.sys.entity.SysOrgEntity;
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 组织分账接收方 Service 实现
|
||||
*/
|
||||
@Service("orgSettlementAccountService")
|
||||
public class OrgSettlementAccountServiceImpl extends ServiceImpl<OrgSettlementAccountDao, OrgSettlementAccountEntity> implements OrgSettlementAccountService {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(OrgSettlementAccountServiceImpl.class);
|
||||
|
||||
@Autowired
|
||||
private SysOrgDao sysOrgDao;
|
||||
|
||||
@Autowired
|
||||
private io.as.modules.payment.service.WechatPayService wechatPayService;
|
||||
|
||||
@Override
|
||||
public PageUtils queryPage(Map<String, Object> params) {
|
||||
IPage<OrgSettlementAccountEntity> page = new Query<OrgSettlementAccountEntity>().getPage(params, "created_at", false);
|
||||
|
||||
QueryWrapper<OrgSettlementAccountEntity> wrapper = new QueryWrapper<>();
|
||||
wrapper.eq("delete_flag", DayCardConstants.DELETE_FLAG_UNDEL);
|
||||
|
||||
String orgId = (String) params.get("orgId");
|
||||
if (StringUtils.isNotBlank(orgId)) {
|
||||
wrapper.eq("org_id", orgId);
|
||||
}
|
||||
String bindStatus = (String) params.get("bindStatus");
|
||||
if (StringUtils.isNotBlank(bindStatus)) {
|
||||
wrapper.eq("bind_status", bindStatus);
|
||||
}
|
||||
String receiverType = (String) params.get("receiverType");
|
||||
if (StringUtils.isNotBlank(receiverType)) {
|
||||
wrapper.eq("receiver_type", receiverType);
|
||||
}
|
||||
|
||||
IPage<OrgSettlementAccountEntity> result = baseMapper.selectPage(page, wrapper);
|
||||
|
||||
// 填充组织名称
|
||||
for (OrgSettlementAccountEntity entity : result.getRecords()) {
|
||||
if (entity.getOrgId() != null) {
|
||||
SysOrgEntity org = sysOrgDao.selectById(entity.getOrgId());
|
||||
if (org != null) {
|
||||
entity.setOrgName(org.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new PageUtils(result);
|
||||
}
|
||||
|
||||
@Override
|
||||
public OrgSettlementAccountEntity getById(Long id) {
|
||||
OrgSettlementAccountEntity entity = baseMapper.selectById(id);
|
||||
if (entity != null && entity.getOrgId() != null) {
|
||||
SysOrgEntity org = sysOrgDao.selectById(entity.getOrgId());
|
||||
if (org != null) {
|
||||
entity.setOrgName(org.getName());
|
||||
}
|
||||
}
|
||||
return entity;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveAccount(OrgSettlementAccountEntity account) {
|
||||
Date now = new Date();
|
||||
account.setDeleteFlag(DayCardConstants.DELETE_FLAG_UNDEL);
|
||||
if (account.getBindStatus() == null) {
|
||||
account.setBindStatus(DayCardConstants.BIND_STATUS_PENDING);
|
||||
}
|
||||
if (account.getPayChannel() == null) {
|
||||
account.setPayChannel(DayCardConstants.PAY_CHANNEL_WECHAT);
|
||||
}
|
||||
account.setCreatedAt(now);
|
||||
account.setUpdatedAt(now);
|
||||
|
||||
// 调用微信添加分账接收方
|
||||
WechatPayResult addResult = wechatPayService.addReceiver(
|
||||
account.getReceiverType(),
|
||||
account.getReceiverAccount(),
|
||||
account.getReceiverName(),
|
||||
"STORE");
|
||||
if (addResult.isSuccess()) {
|
||||
account.setBindStatus(DayCardConstants.BIND_STATUS_ACTIVE);
|
||||
account.setExternalBindId(addResult.getPayNo());
|
||||
} else {
|
||||
account.setBindStatus(DayCardConstants.BIND_STATUS_FAILED);
|
||||
logger.warn("微信添加分账接收方失败: account={}, error={}", account.getReceiverAccount(), addResult.getErrorMsg());
|
||||
}
|
||||
|
||||
baseMapper.insert(account);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(Long[] ids) {
|
||||
if (ids == null || ids.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 查询待删除的实体列表
|
||||
List<OrgSettlementAccountEntity> list = new ArrayList<>();
|
||||
Date now = new Date();
|
||||
for (Long id : ids) {
|
||||
OrgSettlementAccountEntity entity = new OrgSettlementAccountEntity();
|
||||
entity.setId(id);
|
||||
entity.setDeleteFlag(DayCardConstants.DELETE_FLAG_DEL);
|
||||
entity.setUpdatedAt(now);
|
||||
list.add(entity);
|
||||
}
|
||||
|
||||
// 调用微信删除分账接收方
|
||||
for (OrgSettlementAccountEntity entity : list) {
|
||||
// 先查询完整的实体信息
|
||||
OrgSettlementAccountEntity full = baseMapper.selectById(entity.getId());
|
||||
if (full != null && full.getReceiverAccount() != null) {
|
||||
WechatPayResult delResult = wechatPayService.deleteReceiver(
|
||||
full.getReceiverType(), full.getReceiverAccount());
|
||||
if (!delResult.isSuccess()) {
|
||||
logger.warn("微信删除分账接收方失败: account={}, error={}",
|
||||
full.getReceiverAccount(), delResult.getErrorMsg());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.updateBatchById(list);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void syncToWechat(Long id) {
|
||||
OrgSettlementAccountEntity account = baseMapper.selectById(id);
|
||||
if (account == null) {
|
||||
return;
|
||||
}
|
||||
logger.info("同步分账接收方到微信: id={}, orgId={}, receiverAccount={}", id, account.getOrgId(), account.getReceiverAccount());
|
||||
|
||||
WechatPayResult result = wechatPayService.addReceiver(
|
||||
account.getReceiverType(),
|
||||
account.getReceiverAccount(),
|
||||
account.getReceiverName(),
|
||||
"STORE");
|
||||
|
||||
UpdateWrapper<OrgSettlementAccountEntity> update = new UpdateWrapper<>();
|
||||
update.eq("id", id)
|
||||
.set("updated_at", new Date());
|
||||
if (result.isSuccess()) {
|
||||
update.set("bind_status", DayCardConstants.BIND_STATUS_ACTIVE);
|
||||
update.set("external_bind_id", result.getPayNo());
|
||||
} else {
|
||||
update.set("bind_status", DayCardConstants.BIND_STATUS_FAILED);
|
||||
}
|
||||
baseMapper.update(null, update);
|
||||
}
|
||||
|
||||
@Override
|
||||
public OrgSettlementAccountEntity getByOrgId(Long orgId) {
|
||||
QueryWrapper<OrgSettlementAccountEntity> wrapper = new QueryWrapper<>();
|
||||
wrapper.eq("org_id", orgId)
|
||||
.eq("delete_flag", DayCardConstants.DELETE_FLAG_UNDEL)
|
||||
.eq("bind_status", DayCardConstants.BIND_STATUS_ACTIVE)
|
||||
.last("LIMIT 1");
|
||||
return baseMapper.selectOne(wrapper);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
package io.as.modules.daycard.service.impl;
|
||||
|
||||
import io.as.modules.daycard.constants.DayCardConstants;
|
||||
import io.as.modules.daycard.entity.DayCardOrderEntity;
|
||||
import io.as.modules.daycard.entity.DayCardProfitRuleEntity;
|
||||
import io.as.modules.daycard.entity.DayCardSettleOrderEntity;
|
||||
import io.as.modules.daycard.service.DayCardProfitRuleService;
|
||||
import io.as.modules.daycard.service.ProfitCalcService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
|
||||
/**
|
||||
* 分润计算 Service 实现
|
||||
*/
|
||||
@Service("profitCalcService")
|
||||
public class ProfitCalcServiceImpl implements ProfitCalcService {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ProfitCalcServiceImpl.class);
|
||||
|
||||
@Autowired
|
||||
private DayCardProfitRuleService dayCardProfitRuleService;
|
||||
|
||||
@Override
|
||||
public Map<String, Object> calculate(Long partnerId, Long channelOrgId, Long storeOrgId, BigDecimal totalAmount) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("totalAmount", totalAmount);
|
||||
|
||||
List<DayCardProfitRuleEntity> rules = dayCardProfitRuleService.matchRules(partnerId, channelOrgId, storeOrgId);
|
||||
|
||||
List<Map<String, Object>> ruleList = new ArrayList<>();
|
||||
BigDecimal totalSettleAmount = BigDecimal.ZERO;
|
||||
|
||||
for (DayCardProfitRuleEntity rule : rules) {
|
||||
BigDecimal settleAmount = calcSettleAmount(rule, totalAmount);
|
||||
|
||||
Map<String, Object> ruleMap = new HashMap<>();
|
||||
ruleMap.put("ruleId", rule.getId());
|
||||
ruleMap.put("receiverAccount", rule.getReceiverAccount());
|
||||
ruleMap.put("receiverName", rule.getReceiverName());
|
||||
ruleMap.put("shareRatio", rule.getShareRatio());
|
||||
ruleMap.put("shareType", rule.getShareType());
|
||||
ruleMap.put("settleAmount", settleAmount);
|
||||
ruleList.add(ruleMap);
|
||||
|
||||
totalSettleAmount = totalSettleAmount.add(settleAmount);
|
||||
}
|
||||
|
||||
result.put("rules", ruleList);
|
||||
result.put("totalSettleAmount", totalSettleAmount);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<DayCardSettleOrderEntity> buildSettleOrders(String orderNo, DayCardOrderEntity order) {
|
||||
List<DayCardSettleOrderEntity> settleOrders = new ArrayList<>();
|
||||
|
||||
List<DayCardProfitRuleEntity> rules = dayCardProfitRuleService.matchRules(
|
||||
order.getPartnerId(), order.getChannelOrgId(), order.getStoreOrgId());
|
||||
|
||||
if (rules == null || rules.isEmpty()) {
|
||||
logger.info("未匹配到分润规则,跳过分账: orderNo={}", orderNo);
|
||||
return settleOrders;
|
||||
}
|
||||
|
||||
BigDecimal totalAmount = order.getPayAmount() != null ? order.getPayAmount() : order.getAmount();
|
||||
Date now = new Date();
|
||||
|
||||
for (DayCardProfitRuleEntity rule : rules) {
|
||||
BigDecimal settleAmount = calcSettleAmount(rule, totalAmount);
|
||||
|
||||
DayCardSettleOrderEntity settleOrder = new DayCardSettleOrderEntity();
|
||||
settleOrder.setSettleNo(generateSettleNo());
|
||||
settleOrder.setOrderNo(orderNo);
|
||||
settleOrder.setPartnerId(order.getPartnerId());
|
||||
settleOrder.setRuleId(rule.getId());
|
||||
settleOrder.setReceiverAccount(rule.getReceiverAccount());
|
||||
settleOrder.setReceiverName(rule.getReceiverName());
|
||||
settleOrder.setShareRatio(rule.getShareRatio());
|
||||
settleOrder.setShareType(rule.getShareType());
|
||||
settleOrder.setSettleAmount(settleAmount);
|
||||
settleOrder.setTotalAmount(totalAmount);
|
||||
settleOrder.setStatus(DayCardConstants.SETTLE_STATUS_PENDING);
|
||||
settleOrder.setCreatedAt(now);
|
||||
settleOrder.setUpdatedAt(now);
|
||||
|
||||
settleOrders.add(settleOrder);
|
||||
}
|
||||
|
||||
return settleOrders;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算单条规则的分账金额
|
||||
* <p>RATIO: totalAmount * shareRatio</p>
|
||||
* <p>FIXED: shareRatio 直接作为金额</p>
|
||||
*/
|
||||
private BigDecimal calcSettleAmount(DayCardProfitRuleEntity rule, BigDecimal totalAmount) {
|
||||
if (DayCardConstants.SHARE_TYPE_FIXED.equals(rule.getShareType())) {
|
||||
return rule.getShareRatio().setScale(2, RoundingMode.HALF_UP);
|
||||
}
|
||||
// 默认按比例
|
||||
return totalAmount.multiply(rule.getShareRatio()).setScale(2, RoundingMode.HALF_UP);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成分账单号:ST + yyyyMMddHHmmss + 6位随机数
|
||||
*/
|
||||
private String generateSettleNo() {
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
|
||||
String timestamp = sdf.format(new Date());
|
||||
int random = ThreadLocalRandom.current().nextInt(100000, 1000000);
|
||||
return "ST" + timestamp + random;
|
||||
}
|
||||
}
|
||||
+465
@@ -0,0 +1,465 @@
|
||||
package io.as.modules.daycard.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import io.as.modules.daycard.constants.DayCardConstants;
|
||||
import io.as.modules.daycard.dao.DayCardOrderDao;
|
||||
import io.as.modules.daycard.dao.OrgSettlementAccountDao;
|
||||
import io.as.modules.daycard.dao.ProfitSharingDetailDao;
|
||||
import io.as.modules.daycard.dao.ProfitSharingOrderDao;
|
||||
import io.as.modules.daycard.entity.DayCardOrderEntity;
|
||||
import io.as.modules.daycard.entity.OrgSettlementAccountEntity;
|
||||
import io.as.modules.daycard.entity.ProfitSharingDetailEntity;
|
||||
import io.as.modules.daycard.entity.ProfitSharingOrderEntity;
|
||||
import io.as.modules.daycard.entity.ProfitSharingRuleEntity;
|
||||
import io.as.modules.daycard.entity.ProfitSharingRuleItemEntity;
|
||||
import io.as.modules.daycard.service.ProfitSharingExecuteService;
|
||||
import io.as.modules.daycard.service.ProfitSharingRuleService;
|
||||
import io.as.modules.payment.service.WechatPayResult;
|
||||
import io.as.modules.payment.service.WechatPayService;
|
||||
import io.as.modules.sys.dao.SysOrgDao;
|
||||
import io.as.modules.sys.entity.SysOrgEntity;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
|
||||
/**
|
||||
* 分账执行 Service 实现
|
||||
*/
|
||||
@Service("profitSharingExecuteService")
|
||||
public class ProfitSharingExecuteServiceImpl implements ProfitSharingExecuteService {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ProfitSharingExecuteServiceImpl.class);
|
||||
|
||||
private static final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
@Autowired
|
||||
private DayCardOrderDao dayCardOrderDao;
|
||||
|
||||
@Autowired
|
||||
private ProfitSharingOrderDao profitSharingOrderDao;
|
||||
|
||||
@Autowired
|
||||
private ProfitSharingDetailDao profitSharingDetailDao;
|
||||
|
||||
@Autowired(required = false)
|
||||
private ProfitSharingRuleService profitSharingRuleService;
|
||||
|
||||
@Autowired
|
||||
private OrgSettlementAccountDao orgSettlementAccountDao;
|
||||
|
||||
@Autowired
|
||||
private SysOrgDao sysOrgDao;
|
||||
|
||||
@Autowired
|
||||
private io.as.modules.payment.service.WechatPayService wechatPayService;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void executeSharing(String orderNo) {
|
||||
// 1. 查询订单
|
||||
QueryWrapper<DayCardOrderEntity> orderWrapper = new QueryWrapper<>();
|
||||
orderWrapper.eq("order_no", orderNo)
|
||||
.eq("delete_flag", DayCardConstants.DELETE_FLAG_UNDEL);
|
||||
DayCardOrderEntity order = dayCardOrderDao.selectOne(orderWrapper);
|
||||
if (order == null) {
|
||||
logger.warn("分账执行失败:订单不存在, orderNo={}", orderNo);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 幂等检查:查 profit_sharing_order by pay_order_no,已存在则跳过
|
||||
QueryWrapper<ProfitSharingOrderEntity> existWrapper = new QueryWrapper<>();
|
||||
existWrapper.eq("pay_order_no", orderNo);
|
||||
Integer existCount = profitSharingOrderDao.selectCount(existWrapper);
|
||||
if (existCount != null && existCount > 0) {
|
||||
logger.info("分账单已存在,跳过: orderNo={}", orderNo);
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. 从订单获取 storeOrgId, agentOrgId, channelOrgId, orgPathSnapshot
|
||||
Long storeOrgId = order.getStoreOrgId();
|
||||
Long channelOrgId = order.getChannelOrgId();
|
||||
Long agentOrgId = resolveAgentOrgId(storeOrgId);
|
||||
Long platformOrgId = resolvePlatformOrgId(storeOrgId);
|
||||
String orgPathSnapshot = buildOrgPathSnapshot(storeOrgId, agentOrgId, channelOrgId);
|
||||
|
||||
// 4. 调用 profitSharingRuleService.matchRule() 匹配规则
|
||||
if (profitSharingRuleService == null) {
|
||||
logger.warn("ProfitSharingRuleService 未注入,跳过分账: orderNo={}", orderNo);
|
||||
return;
|
||||
}
|
||||
|
||||
ProfitSharingRuleEntity rule = profitSharingRuleService.matchRule(storeOrgId, agentOrgId, channelOrgId);
|
||||
if (rule == null || rule.getItems() == null || rule.getItems().isEmpty()) {
|
||||
logger.info("无匹配的分账规则,跳过分账: orderNo={}", orderNo);
|
||||
return;
|
||||
}
|
||||
|
||||
// 5. 获取 rule_items,计算各方分账金额
|
||||
// 金额单位:分(int),从订单 amount(元)转换
|
||||
BigDecimal amountYuan = order.getAmount();
|
||||
if (amountYuan == null) {
|
||||
logger.warn("订单金额为空,跳过分账: orderNo={}", orderNo);
|
||||
return;
|
||||
}
|
||||
int totalAmount = amountYuan.multiply(new BigDecimal("100")).intValue();
|
||||
|
||||
List<ProfitSharingRuleItemEntity> ruleItems = rule.getItems();
|
||||
List<ProfitSharingDetailEntity> details = new ArrayList<>();
|
||||
|
||||
int nonPlatformSum = 0;
|
||||
BigDecimal nonPlatformTotalRatio = BigDecimal.ZERO;
|
||||
|
||||
for (ProfitSharingRuleItemEntity item : ruleItems) {
|
||||
ProfitSharingDetailEntity detail = new ProfitSharingDetailEntity();
|
||||
detail.setOrgRole(item.getOrgRole());
|
||||
detail.setOrgId(resolveOrgIdByRole(item.getOrgRole(), platformOrgId, channelOrgId, agentOrgId, storeOrgId));
|
||||
detail.setStatus(DayCardConstants.SETTLE_STATUS_PENDING);
|
||||
|
||||
if (DayCardConstants.SHARE_TYPE_FIXED.equals(item.getShareType())) {
|
||||
// FIXED: shareValue 即为金额(分)
|
||||
detail.setAmount(item.getShareValue().intValue());
|
||||
} else {
|
||||
// RATIO: totalAmount * shareValue / 100
|
||||
int amt = BigDecimal.valueOf(totalAmount)
|
||||
.multiply(item.getShareValue())
|
||||
.divide(BigDecimal.valueOf(100), 0, RoundingMode.DOWN)
|
||||
.intValue();
|
||||
detail.setAmount(amt);
|
||||
}
|
||||
|
||||
detail.setDescription("分账-" + (item.getOrgRole() != null ? item.getOrgRole() : "UNKNOWN"));
|
||||
|
||||
// 累计非平台方金额和比例
|
||||
if (!DayCardConstants.ORG_ROLE_PLATFORM.equals(item.getOrgRole())) {
|
||||
nonPlatformSum += detail.getAmount();
|
||||
if (DayCardConstants.SHARE_TYPE_RATIO.equals(item.getShareType()) && item.getShareValue() != null) {
|
||||
nonPlatformTotalRatio = nonPlatformTotalRatio.add(item.getShareValue());
|
||||
}
|
||||
}
|
||||
|
||||
details.add(detail);
|
||||
}
|
||||
|
||||
// 尾差归平台:PLATFORM 的金额 = totalAmount - 其他各方之和
|
||||
for (ProfitSharingDetailEntity detail : details) {
|
||||
if (DayCardConstants.ORG_ROLE_PLATFORM.equals(detail.getOrgRole())) {
|
||||
detail.setAmount(totalAmount - nonPlatformSum);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 6. 二次校验非平台方总比例 ≤ 30%
|
||||
if (nonPlatformTotalRatio.compareTo(BigDecimal.valueOf(DayCardConstants.PROFIT_SHARE_LIMIT)) > 0) {
|
||||
logger.error("非平台方总比例超过30%限制: orderNo={}, ratioSum={}", orderNo, nonPlatformTotalRatio);
|
||||
return;
|
||||
}
|
||||
|
||||
// 7. 创建 profit_sharing_order(存 rule_snapshot JSON)
|
||||
String outOrderNo = generateOutOrderNo();
|
||||
String ruleSnapshot = serializeRuleSnapshot(rule);
|
||||
|
||||
ProfitSharingOrderEntity sharingOrder = new ProfitSharingOrderEntity();
|
||||
sharingOrder.setPayOrderNo(orderNo);
|
||||
sharingOrder.setTransactionId(order.getPayNo());
|
||||
sharingOrder.setOutOrderNo(outOrderNo);
|
||||
sharingOrder.setStatus(DayCardConstants.SETTLE_STATUS_PENDING);
|
||||
sharingOrder.setTotalAmount(totalAmount);
|
||||
sharingOrder.setRuleId(rule.getId());
|
||||
sharingOrder.setRuleSnapshot(ruleSnapshot);
|
||||
sharingOrder.setOrgPathSnapshot(orgPathSnapshot);
|
||||
sharingOrder.setSharingChannel("WECHAT_NATIVE");
|
||||
sharingOrder.setDeleteFlag(DayCardConstants.DELETE_FLAG_UNDEL);
|
||||
Date now = new Date();
|
||||
sharingOrder.setCreatedAt(now);
|
||||
sharingOrder.setUpdatedAt(now);
|
||||
profitSharingOrderDao.insert(sharingOrder);
|
||||
|
||||
// 8. 批量创建 profit_sharing_detail
|
||||
for (ProfitSharingDetailEntity detail : details) {
|
||||
detail.setSharingOrderId(sharingOrder.getId());
|
||||
detail.setCreatedAt(now);
|
||||
detail.setUpdatedAt(now);
|
||||
profitSharingDetailDao.insert(detail);
|
||||
}
|
||||
|
||||
logger.info("分账单已创建: orderNo={}, outOrderNo={}, detailCount={}", orderNo, outOrderNo, details.size());
|
||||
|
||||
// 9. 查询各接收方的 org_settlement_account(bind_status=ACTIVE)
|
||||
for (ProfitSharingDetailEntity detail : details) {
|
||||
if (detail.getOrgId() != null) {
|
||||
OrgSettlementAccountEntity account = orgSettlementAccountDao.selectOne(
|
||||
new QueryWrapper<OrgSettlementAccountEntity>()
|
||||
.eq("org_id", detail.getOrgId())
|
||||
.eq("delete_flag", DayCardConstants.DELETE_FLAG_UNDEL)
|
||||
.eq("bind_status", DayCardConstants.BIND_STATUS_ACTIVE)
|
||||
.last("LIMIT 1"));
|
||||
if (account != null) {
|
||||
detail.setReceiverAccount(account.getReceiverAccount());
|
||||
detail.setReceiverType(account.getReceiverType());
|
||||
} else {
|
||||
logger.warn("未找到活跃的结算账户: orgId={}, orgRole={}", detail.getOrgId(), detail.getOrgRole());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 10. 调用微信分账 API
|
||||
// 更新分账单状态为 PROCESSING
|
||||
UpdateWrapper<ProfitSharingOrderEntity> orderUpdate = new UpdateWrapper<>();
|
||||
orderUpdate.eq("id", sharingOrder.getId())
|
||||
.set("status", DayCardConstants.SETTLE_STATUS_PROCESSING)
|
||||
.set("updated_at", new Date());
|
||||
profitSharingOrderDao.update(null, orderUpdate);
|
||||
|
||||
// 构建分账接收方列表
|
||||
List<Map<String, Object>> receivers = new ArrayList<>();
|
||||
for (ProfitSharingDetailEntity detail : details) {
|
||||
if (detail.getReceiverAccount() == null || DayCardConstants.ORG_ROLE_PLATFORM.equals(detail.getOrgRole())) {
|
||||
continue; // 平台方通过完结分账获取留成,不需要分账接收方
|
||||
}
|
||||
Map<String, Object> receiver = new HashMap<>();
|
||||
receiver.put("type", detail.getReceiverType() != null ? detail.getReceiverType() : "MERCHANT_ID");
|
||||
receiver.put("account", detail.getReceiverAccount());
|
||||
receiver.put("amount", detail.getAmount());
|
||||
receiver.put("description", detail.getDescription() != null ? detail.getDescription() : "分账");
|
||||
receivers.add(receiver);
|
||||
}
|
||||
|
||||
// 调用微信分账 API
|
||||
String transactionId = order.getPayNo();
|
||||
WechatPayResult sharingResult = wechatPayService.createProfitSharing(transactionId, outOrderNo, receivers);
|
||||
if (!sharingResult.isSuccess()) {
|
||||
// 分账失败,更新状态
|
||||
UpdateWrapper<ProfitSharingOrderEntity> failUpdate = new UpdateWrapper<>();
|
||||
failUpdate.eq("id", sharingOrder.getId())
|
||||
.set("status", DayCardConstants.SETTLE_STATUS_FAILED)
|
||||
.set("updated_at", new Date());
|
||||
profitSharingOrderDao.update(null, failUpdate);
|
||||
logger.error("微信分账失败: orderNo={}, outOrderNo={}, error={}", orderNo, outOrderNo, sharingResult.getErrorMsg());
|
||||
return;
|
||||
}
|
||||
logger.info("微信分账API调用成功: outOrderNo={}", outOrderNo);
|
||||
|
||||
// 模拟/实际更新所有 detail 状态为 SUCCESS
|
||||
for (ProfitSharingDetailEntity detail : details) {
|
||||
UpdateWrapper<ProfitSharingDetailEntity> detailUpdate = new UpdateWrapper<>();
|
||||
detailUpdate.eq("id", detail.getId())
|
||||
.set("status", DayCardConstants.SETTLE_STATUS_SUCCESS)
|
||||
.set("success_time", new Date())
|
||||
.set("updated_at", new Date());
|
||||
profitSharingDetailDao.update(null, detailUpdate);
|
||||
}
|
||||
|
||||
// 11. 调用完结分账 API
|
||||
boolean finishSuccess = wechatPayService.finishProfitSharing(outOrderNo, transactionId);
|
||||
if (finishSuccess) {
|
||||
UpdateWrapper<ProfitSharingOrderEntity> finishUpdate = new UpdateWrapper<>();
|
||||
finishUpdate.eq("id", sharingOrder.getId())
|
||||
.set("status", DayCardConstants.SETTLE_STATUS_SUCCESS)
|
||||
.set("updated_at", new Date());
|
||||
profitSharingOrderDao.update(null, finishUpdate);
|
||||
logger.info("分账执行成功: orderNo={}, outOrderNo={}", orderNo, outOrderNo);
|
||||
} else {
|
||||
logger.warn("完结分账失败,分账单保持PROCESSING状态: outOrderNo={}", outOrderNo);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void retryFailed() {
|
||||
// 1. 查 profit_sharing_order status=PENDING 或 FAILED 且 created_at < NOW() - 5分钟
|
||||
Date threshold = new Date(System.currentTimeMillis() - 5 * 60 * 1000L);
|
||||
QueryWrapper<ProfitSharingOrderEntity> wrapper = new QueryWrapper<>();
|
||||
wrapper.in("status", DayCardConstants.SETTLE_STATUS_PENDING, DayCardConstants.SETTLE_STATUS_FAILED)
|
||||
.eq("delete_flag", DayCardConstants.DELETE_FLAG_UNDEL)
|
||||
.lt("created_at", threshold)
|
||||
.last("LIMIT 200");
|
||||
List<ProfitSharingOrderEntity> failedOrders = profitSharingOrderDao.selectList(wrapper);
|
||||
|
||||
if (failedOrders.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info("开始重试失败的分账单,共 {} 笔", failedOrders.size());
|
||||
|
||||
int retried = 0;
|
||||
for (ProfitSharingOrderEntity sharingOrder : failedOrders) {
|
||||
try {
|
||||
retrySingle(sharingOrder);
|
||||
retried++;
|
||||
} catch (Exception e) {
|
||||
logger.error("重试分账单失败: outOrderNo={}", sharingOrder.getOutOrderNo(), e);
|
||||
}
|
||||
}
|
||||
|
||||
logger.info("重试分账单完成,成功 {} 笔", retried);
|
||||
}
|
||||
|
||||
/**
|
||||
* 重试单笔分账单
|
||||
*/
|
||||
private void retrySingle(ProfitSharingOrderEntity sharingOrder) {
|
||||
// 查询明细
|
||||
QueryWrapper<ProfitSharingDetailEntity> wrapper = new QueryWrapper<>();
|
||||
wrapper.eq("sharing_order_id", sharingOrder.getId());
|
||||
List<ProfitSharingDetailEntity> details = profitSharingDetailDao.selectList(wrapper);
|
||||
|
||||
// 更新分账单状态为 PROCESSING
|
||||
UpdateWrapper<ProfitSharingOrderEntity> orderUpdate = new UpdateWrapper<>();
|
||||
orderUpdate.eq("id", sharingOrder.getId())
|
||||
.set("status", DayCardConstants.SETTLE_STATUS_PROCESSING)
|
||||
.set("updated_at", new Date());
|
||||
profitSharingOrderDao.update(null, orderUpdate);
|
||||
|
||||
// 调用微信查询分账状态
|
||||
WechatPayResult queryResult = wechatPayService.queryProfitSharing(sharingOrder.getOutOrderNo());
|
||||
if (queryResult.isSuccess()) {
|
||||
logger.info("微信分账查询成功: outOrderNo={}", sharingOrder.getOutOrderNo());
|
||||
} else {
|
||||
logger.warn("微信分账查询失败: outOrderNo={}, error={}", sharingOrder.getOutOrderNo(), queryResult.getErrorMsg());
|
||||
}
|
||||
|
||||
// 占位:模拟调用微信分账 API 成功
|
||||
logger.info("重试分账(占位): outOrderNo={}", sharingOrder.getOutOrderNo());
|
||||
|
||||
// 更新所有 detail 状态为 SUCCESS
|
||||
for (ProfitSharingDetailEntity detail : details) {
|
||||
if (!DayCardConstants.SETTLE_STATUS_SUCCESS.equals(detail.getStatus())) {
|
||||
UpdateWrapper<ProfitSharingDetailEntity> detailUpdate = new UpdateWrapper<>();
|
||||
detailUpdate.eq("id", detail.getId())
|
||||
.set("status", DayCardConstants.SETTLE_STATUS_SUCCESS)
|
||||
.set("success_time", new Date())
|
||||
.set("updated_at", new Date());
|
||||
profitSharingDetailDao.update(null, detailUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
// 更新分账单状态为 SUCCESS
|
||||
UpdateWrapper<ProfitSharingOrderEntity> finishUpdate = new UpdateWrapper<>();
|
||||
finishUpdate.eq("id", sharingOrder.getId())
|
||||
.set("status", DayCardConstants.SETTLE_STATUS_SUCCESS)
|
||||
.set("updated_at", new Date());
|
||||
profitSharingOrderDao.update(null, finishUpdate);
|
||||
|
||||
logger.info("重试分账成功: outOrderNo={}", sharingOrder.getOutOrderNo());
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析代理组织ID(门店的父组织)
|
||||
*/
|
||||
private Long resolveAgentOrgId(Long storeOrgId) {
|
||||
if (storeOrgId == null) {
|
||||
return null;
|
||||
}
|
||||
SysOrgEntity storeOrg = sysOrgDao.selectById(storeOrgId);
|
||||
if (storeOrg == null || storeOrg.getParentId() == null) {
|
||||
return null;
|
||||
}
|
||||
return storeOrg.getParentId();
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析平台组织ID(mpath 第一段)
|
||||
*/
|
||||
private Long resolvePlatformOrgId(Long storeOrgId) {
|
||||
if (storeOrgId == null) {
|
||||
return null;
|
||||
}
|
||||
SysOrgEntity storeOrg = sysOrgDao.selectById(storeOrgId);
|
||||
if (storeOrg == null || storeOrg.getMpath() == null) {
|
||||
return null;
|
||||
}
|
||||
String[] segments = storeOrg.getMpath().split("\\.");
|
||||
if (segments.length >= 1) {
|
||||
return Long.parseLong(segments[0]);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据组织角色解析对应的组织ID
|
||||
*/
|
||||
private Long resolveOrgIdByRole(String orgRole, Long platformOrgId, Long channelOrgId, Long agentOrgId, Long storeOrgId) {
|
||||
if (orgRole == null) {
|
||||
return null;
|
||||
}
|
||||
switch (orgRole) {
|
||||
case DayCardConstants.ORG_ROLE_PLATFORM:
|
||||
return platformOrgId;
|
||||
case DayCardConstants.ORG_ROLE_CHANNEL:
|
||||
return channelOrgId;
|
||||
case DayCardConstants.ORG_ROLE_AGENT:
|
||||
return agentOrgId;
|
||||
case DayCardConstants.ORG_ROLE_STORE:
|
||||
return storeOrgId;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建组织链路快照
|
||||
*/
|
||||
private String buildOrgPathSnapshot(Long storeOrgId, Long agentOrgId, Long channelOrgId) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
if (channelOrgId != null) {
|
||||
SysOrgEntity channelOrg = sysOrgDao.selectById(channelOrgId);
|
||||
if (channelOrg != null) {
|
||||
sb.append(channelOrg.getName());
|
||||
}
|
||||
}
|
||||
if (agentOrgId != null) {
|
||||
SysOrgEntity agentOrg = sysOrgDao.selectById(agentOrgId);
|
||||
if (agentOrg != null) {
|
||||
if (sb.length() > 0) {
|
||||
sb.append(" > ");
|
||||
}
|
||||
sb.append(agentOrg.getName());
|
||||
}
|
||||
}
|
||||
if (storeOrgId != null) {
|
||||
SysOrgEntity storeOrg = sysOrgDao.selectById(storeOrgId);
|
||||
if (storeOrg != null) {
|
||||
if (sb.length() > 0) {
|
||||
sb.append(" > ");
|
||||
}
|
||||
sb.append(storeOrg.getName());
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 序列化规则快照为 JSON
|
||||
*/
|
||||
private String serializeRuleSnapshot(ProfitSharingRuleEntity rule) {
|
||||
try {
|
||||
return objectMapper.writeValueAsString(rule);
|
||||
} catch (Exception e) {
|
||||
logger.warn("序列化规则快照失败: ruleId={}", rule.getId(), e);
|
||||
return "{}";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成分账内部单号:PS + yyyyMMddHHmmss + 6位随机数
|
||||
*/
|
||||
private String generateOutOrderNo() {
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
|
||||
String timestamp = sdf.format(new Date());
|
||||
int random = ThreadLocalRandom.current().nextInt(100000, 1000000);
|
||||
return "PS" + timestamp + random;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
package io.as.modules.daycard.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import io.as.common.exception.ASException;
|
||||
import io.as.common.utils.PageUtils;
|
||||
import io.as.common.utils.Query;
|
||||
import io.as.modules.daycard.constants.DayCardConstants;
|
||||
import io.as.modules.daycard.dao.ProfitSharingRuleDao;
|
||||
import io.as.modules.daycard.dao.ProfitSharingRuleItemDao;
|
||||
import io.as.modules.daycard.entity.ProfitSharingRuleEntity;
|
||||
import io.as.modules.daycard.entity.ProfitSharingRuleItemEntity;
|
||||
import io.as.modules.daycard.service.ProfitSharingRuleService;
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 分润规则 Service 实现
|
||||
*/
|
||||
@Service("profitSharingRuleService")
|
||||
public class ProfitSharingRuleServiceImpl extends ServiceImpl<ProfitSharingRuleDao, ProfitSharingRuleEntity> implements ProfitSharingRuleService {
|
||||
|
||||
@Autowired
|
||||
private ProfitSharingRuleItemDao profitSharingRuleItemDao;
|
||||
|
||||
@Override
|
||||
public PageUtils queryPage(Map<String, Object> params) {
|
||||
IPage<ProfitSharingRuleEntity> page = new Query<ProfitSharingRuleEntity>().getPage(params, "created_at", false);
|
||||
|
||||
QueryWrapper<ProfitSharingRuleEntity> wrapper = new QueryWrapper<>();
|
||||
wrapper.eq("delete_flag", DayCardConstants.DELETE_FLAG_UNDEL);
|
||||
|
||||
String scopeType = (String) params.get("scopeType");
|
||||
if (StringUtils.isNotBlank(scopeType)) {
|
||||
wrapper.eq("scope_type", scopeType);
|
||||
}
|
||||
String status = (String) params.get("status");
|
||||
if (StringUtils.isNotBlank(status)) {
|
||||
wrapper.eq("status", status);
|
||||
}
|
||||
|
||||
IPage<ProfitSharingRuleEntity> result = baseMapper.selectPage(page, wrapper);
|
||||
return new PageUtils(result);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProfitSharingRuleEntity getInfo(Long id) {
|
||||
ProfitSharingRuleEntity rule = baseMapper.selectById(id);
|
||||
if (rule == null) {
|
||||
return null;
|
||||
}
|
||||
// 查询子表 items
|
||||
QueryWrapper<ProfitSharingRuleItemEntity> itemWrapper = new QueryWrapper<>();
|
||||
itemWrapper.eq("rule_id", id).orderByAsc("sort_order");
|
||||
List<ProfitSharingRuleItemEntity> items = profitSharingRuleItemDao.selectList(itemWrapper);
|
||||
rule.setItems(items);
|
||||
return rule;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void saveRule(ProfitSharingRuleEntity rule) {
|
||||
Date now = new Date();
|
||||
rule.setDeleteFlag(DayCardConstants.DELETE_FLAG_UNDEL);
|
||||
rule.setCreatedAt(now);
|
||||
rule.setUpdatedAt(now);
|
||||
// 1. 插入主表获取 id
|
||||
baseMapper.insert(rule);
|
||||
|
||||
// 2. 批量插入 items
|
||||
List<ProfitSharingRuleItemEntity> items = rule.getItems();
|
||||
if (items != null && !items.isEmpty()) {
|
||||
for (ProfitSharingRuleItemEntity item : items) {
|
||||
item.setRuleId(rule.getId());
|
||||
item.setCreatedAt(now);
|
||||
item.setUpdatedAt(now);
|
||||
profitSharingRuleItemDao.insert(item);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 30%校验
|
||||
validateShareRatio(items);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void updateRule(ProfitSharingRuleEntity rule) {
|
||||
Date now = new Date();
|
||||
rule.setUpdatedAt(now);
|
||||
// 1. 更新主表
|
||||
baseMapper.updateById(rule);
|
||||
|
||||
// 2. 删除旧 items
|
||||
profitSharingRuleItemDao.delete(new QueryWrapper<ProfitSharingRuleItemEntity>().eq("rule_id", rule.getId()));
|
||||
|
||||
// 3. 批量插入新 items
|
||||
List<ProfitSharingRuleItemEntity> items = rule.getItems();
|
||||
if (items != null && !items.isEmpty()) {
|
||||
for (ProfitSharingRuleItemEntity item : items) {
|
||||
item.setRuleId(rule.getId());
|
||||
item.setCreatedAt(now);
|
||||
item.setUpdatedAt(now);
|
||||
profitSharingRuleItemDao.insert(item);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 30%校验
|
||||
validateShareRatio(items);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(Long id) {
|
||||
ProfitSharingRuleEntity entity = new ProfitSharingRuleEntity();
|
||||
entity.setId(id);
|
||||
entity.setDeleteFlag(DayCardConstants.DELETE_FLAG_DEL);
|
||||
entity.setUpdatedAt(new Date());
|
||||
baseMapper.updateById(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProfitSharingRuleEntity matchRule(Long storeOrgId, Long agentOrgId, Long channelOrgId) {
|
||||
// 1. 查 STORE 级规则
|
||||
if (storeOrgId != null) {
|
||||
ProfitSharingRuleEntity rule = queryActiveRule(DayCardConstants.SCOPE_STORE, storeOrgId);
|
||||
if (rule != null) {
|
||||
fillItems(rule);
|
||||
return rule;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 未命中 -> 查 AGENT 级
|
||||
if (agentOrgId != null) {
|
||||
ProfitSharingRuleEntity rule = queryActiveRule(DayCardConstants.SCOPE_AGENT, agentOrgId);
|
||||
if (rule != null) {
|
||||
fillItems(rule);
|
||||
return rule;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 未命中 -> 查 CHANNEL 级
|
||||
if (channelOrgId != null) {
|
||||
ProfitSharingRuleEntity rule = queryActiveRule(DayCardConstants.SCOPE_CHANNEL, channelOrgId);
|
||||
if (rule != null) {
|
||||
fillItems(rule);
|
||||
return rule;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 未命中 -> 查 GLOBAL 级
|
||||
ProfitSharingRuleEntity rule = queryActiveRule(DayCardConstants.SCOPE_GLOBAL, null);
|
||||
if (rule != null) {
|
||||
fillItems(rule);
|
||||
return rule;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询指定范围内生效的规则(单条,按 priority DESC 取最高)
|
||||
*/
|
||||
private ProfitSharingRuleEntity queryActiveRule(String scopeType, Long scopeOrgId) {
|
||||
QueryWrapper<ProfitSharingRuleEntity> wrapper = new QueryWrapper<>();
|
||||
wrapper.eq("scope_type", scopeType)
|
||||
.eq("status", DayCardConstants.PROFIT_RULE_STATUS_ACTIVE)
|
||||
.eq("delete_flag", DayCardConstants.DELETE_FLAG_UNDEL)
|
||||
.and(w -> w.isNull("effective_from").or().le("effective_from", new Date()))
|
||||
.and(w -> w.isNull("effective_to").or().ge("effective_to", new Date()))
|
||||
.orderByDesc("priority")
|
||||
.last("LIMIT 1");
|
||||
if (scopeOrgId != null) {
|
||||
wrapper.eq("scope_org_id", scopeOrgId);
|
||||
} else {
|
||||
wrapper.isNull("scope_org_id");
|
||||
}
|
||||
return baseMapper.selectOne(wrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* 填充规则的 items 子表
|
||||
*/
|
||||
private void fillItems(ProfitSharingRuleEntity rule) {
|
||||
QueryWrapper<ProfitSharingRuleItemEntity> itemWrapper = new QueryWrapper<>();
|
||||
itemWrapper.eq("rule_id", rule.getId()).orderByAsc("sort_order");
|
||||
List<ProfitSharingRuleItemEntity> items = profitSharingRuleItemDao.selectList(itemWrapper);
|
||||
rule.setItems(items);
|
||||
}
|
||||
|
||||
/**
|
||||
* 30%校验:计算非平台方(orgRole != PLATFORM)的 RATIO 类型 shareValue 之和,超过30则抛异常
|
||||
*/
|
||||
private void validateShareRatio(List<ProfitSharingRuleItemEntity> items) {
|
||||
if (items == null || items.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
BigDecimal sum = BigDecimal.ZERO;
|
||||
for (ProfitSharingRuleItemEntity item : items) {
|
||||
if (!DayCardConstants.ORG_ROLE_PLATFORM.equals(item.getOrgRole())
|
||||
&& DayCardConstants.SHARE_TYPE_RATIO.equals(item.getShareType())) {
|
||||
if (item.getShareValue() != null) {
|
||||
sum = sum.add(item.getShareValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
if (sum.compareTo(BigDecimal.valueOf(DayCardConstants.PROFIT_SHARE_LIMIT)) > 0) {
|
||||
throw new ASException("非平台方分润比例之和不能超过" + DayCardConstants.PROFIT_SHARE_LIMIT + "%");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
package io.as.modules.daycard.service.impl;
|
||||
|
||||
import io.as.modules.daycard.constants.DayCardConstants;
|
||||
import io.as.modules.daycard.dao.DayCardOrderDao;
|
||||
import io.as.modules.daycard.entity.DayCardOrderEntity;
|
||||
import io.as.modules.daycard.entity.DayCardSettleOrderEntity;
|
||||
import io.as.modules.daycard.service.DayCardSettleOrderService;
|
||||
import io.as.modules.daycard.service.ProfitCalcService;
|
||||
import io.as.modules.daycard.service.SettleExecuteService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 分账执行 Service 实现
|
||||
*/
|
||||
@Service("settleExecuteService")
|
||||
public class SettleExecuteServiceImpl implements SettleExecuteService {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(SettleExecuteServiceImpl.class);
|
||||
|
||||
@Autowired
|
||||
private DayCardOrderDao dayCardOrderDao;
|
||||
|
||||
@Autowired
|
||||
private ProfitCalcService profitCalcService;
|
||||
|
||||
@Autowired
|
||||
private DayCardSettleOrderService dayCardSettleOrderService;
|
||||
|
||||
@Override
|
||||
public void executeSettle(String orderNo) {
|
||||
// 1. 查询订单
|
||||
DayCardOrderEntity order = dayCardOrderDao.selectOne(
|
||||
new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<DayCardOrderEntity>()
|
||||
.eq("order_no", orderNo)
|
||||
.eq("delete_flag", DayCardConstants.DELETE_FLAG_UNDEL));
|
||||
if (order == null) {
|
||||
logger.warn("分账执行失败:订单不存在, orderNo={}", orderNo);
|
||||
return;
|
||||
}
|
||||
|
||||
// 幂等检查:已有分账单则跳过
|
||||
List<DayCardSettleOrderEntity> existingOrders = dayCardSettleOrderService.getByOrderNo(orderNo);
|
||||
if (existingOrders != null && !existingOrders.isEmpty()) {
|
||||
logger.info("分账单已存在,跳过: orderNo={}", orderNo);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 调用 ProfitCalcService 构建分账单
|
||||
List<DayCardSettleOrderEntity> settleOrders = profitCalcService.buildSettleOrders(orderNo, order);
|
||||
if (settleOrders == null || settleOrders.isEmpty()) {
|
||||
logger.info("无匹配的分润规则,跳过分账: orderNo={}", orderNo);
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. 保存分账单
|
||||
for (DayCardSettleOrderEntity settleOrder : settleOrders) {
|
||||
dayCardSettleOrderService.saveSettleOrder(settleOrder);
|
||||
}
|
||||
logger.info("分账单已保存: orderNo={}, count={}", orderNo, settleOrders.size());
|
||||
|
||||
// 4. 逐条执行
|
||||
for (DayCardSettleOrderEntity settleOrder : settleOrders) {
|
||||
try {
|
||||
executeSingle(settleOrder);
|
||||
} catch (Exception e) {
|
||||
logger.error("执行分账单失败: settleNo={}", settleOrder.getSettleNo(), e);
|
||||
dayCardSettleOrderService.updateStatus(settleOrder.getId(),
|
||||
DayCardConstants.SETTLE_STATUS_FAILED, e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void executeSingle(DayCardSettleOrderEntity settleOrder) {
|
||||
// 设置状态为处理中
|
||||
dayCardSettleOrderService.updateStatus(settleOrder.getId(),
|
||||
DayCardConstants.SETTLE_STATUS_PROCESSING, null);
|
||||
|
||||
try {
|
||||
// TODO: 实际调用微信分账API
|
||||
// 当前为占位:模拟调用成功
|
||||
logger.info("执行分账(占位): settleNo={}, receiverAccount={}, settleAmount={}",
|
||||
settleOrder.getSettleNo(), settleOrder.getReceiverAccount(), settleOrder.getSettleAmount());
|
||||
|
||||
// 模拟调用成功
|
||||
dayCardSettleOrderService.updateStatus(settleOrder.getId(),
|
||||
DayCardConstants.SETTLE_STATUS_SUCCESS, null);
|
||||
|
||||
logger.info("分账执行成功: settleNo={}", settleOrder.getSettleNo());
|
||||
} catch (Exception e) {
|
||||
logger.error("分账执行失败: settleNo={}", settleOrder.getSettleNo(), e);
|
||||
dayCardSettleOrderService.updateStatus(settleOrder.getId(),
|
||||
DayCardConstants.SETTLE_STATUS_FAILED, e.getMessage());
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int retryFailed() {
|
||||
// 先重置失败的分账单状态
|
||||
int resetCount = dayCardSettleOrderService.retryFailed();
|
||||
if (resetCount == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
logger.info("开始重试失败的分账单,共 {} 笔", resetCount);
|
||||
|
||||
// 查询 PENDING 状态的分账单重新执行
|
||||
com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<DayCardSettleOrderEntity> wrapper =
|
||||
new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<>();
|
||||
wrapper.eq("status", DayCardConstants.SETTLE_STATUS_PENDING)
|
||||
.last("LIMIT 200");
|
||||
List<DayCardSettleOrderEntity> pendingList = dayCardSettleOrderService.list(wrapper);
|
||||
|
||||
int retried = 0;
|
||||
for (DayCardSettleOrderEntity settleOrder : pendingList) {
|
||||
try {
|
||||
executeSingle(settleOrder);
|
||||
retried++;
|
||||
} catch (Exception e) {
|
||||
logger.error("重试分账单失败: settleNo={}", settleOrder.getSettleNo(), e);
|
||||
}
|
||||
}
|
||||
|
||||
logger.info("重试分账单完成,成功 {} 笔", retried);
|
||||
return retried;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package io.as.modules.daycard.task;
|
||||
|
||||
import io.as.modules.daycard.service.DayCardOrderService;
|
||||
import io.as.modules.job.task.ITask;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 天卡激活失败补偿定时任务
|
||||
* <p>定时扫描 activate_status=FAILED 的卡,重新激活</p>
|
||||
*/
|
||||
@Component("dayCardActivateRetryTask")
|
||||
public class DayCardActivateRetryTask implements ITask {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(DayCardActivateRetryTask.class);
|
||||
|
||||
@Autowired
|
||||
private DayCardOrderService dayCardOrderService;
|
||||
|
||||
@Override
|
||||
public void run(String params) {
|
||||
logger.info("天卡激活失败补偿任务开始执行, params={}", params);
|
||||
try {
|
||||
int retried = dayCardOrderService.retryFailedActivations();
|
||||
logger.info("天卡激活失败补偿任务执行完毕, 重试激活数={}", retried);
|
||||
} catch (Exception e) {
|
||||
logger.error("天卡激活失败补偿任务执行异常", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package io.as.modules.daycard.task;
|
||||
|
||||
import io.as.modules.daycard.service.DayCardOrderService;
|
||||
import io.as.modules.job.task.ITask;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 天卡订单超时取消定时任务
|
||||
* <p>定时扫描状态为 CREATED 且已过期的天卡订单,逐个取消并释放预留的卡</p>
|
||||
*/
|
||||
@Component("dayCardOrderTimeoutTask")
|
||||
public class DayCardOrderTimeoutTask implements ITask {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(DayCardOrderTimeoutTask.class);
|
||||
|
||||
@Autowired
|
||||
private DayCardOrderService dayCardOrderService;
|
||||
|
||||
@Override
|
||||
public void run(String params) {
|
||||
logger.info("天卡订单超时取消任务开始执行, params={}", params);
|
||||
try {
|
||||
int cancelled = dayCardOrderService.cancelTimeoutOrders();
|
||||
logger.info("天卡订单超时取消任务执行完毕, 取消订单数={}", cancelled);
|
||||
} catch (Exception e) {
|
||||
logger.error("天卡订单超时取消任务执行异常", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package io.as.modules.daycard.task;
|
||||
|
||||
import io.as.modules.daycard.service.ProfitSharingExecuteService;
|
||||
import io.as.modules.job.task.ITask;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 分账兜底查询定时任务
|
||||
* <p>定时扫描状态为 PENDING 或 FAILED 且超过5分钟的分账单,逐条重试</p>
|
||||
*/
|
||||
@Component("profitSharingQueryTask")
|
||||
public class ProfitSharingQueryTask implements ITask {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ProfitSharingQueryTask.class);
|
||||
|
||||
@Autowired
|
||||
private ProfitSharingExecuteService profitSharingExecuteService;
|
||||
|
||||
@Override
|
||||
public void run(String params) {
|
||||
logger.info("分账兜底查询任务开始执行, params={}", params);
|
||||
try {
|
||||
profitSharingExecuteService.retryFailed();
|
||||
logger.info("分账兜底查询任务执行完毕");
|
||||
} catch (Exception e) {
|
||||
logger.error("分账兜底查询任务执行异常", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package io.as.modules.daycard.vo;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 天卡订单展示VO
|
||||
*/
|
||||
@Data
|
||||
public class DayCardOrderVo implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private Long id;
|
||||
private String orderNo;
|
||||
private String status;
|
||||
private BigDecimal amount;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date payTime;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date createdAt;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package io.as.modules.daycard.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 天卡池库存统计VO
|
||||
*/
|
||||
@Data
|
||||
public class DayCardPoolStatsVo implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 总数 */
|
||||
private Integer total;
|
||||
/** 可用(未使用) */
|
||||
private Integer available;
|
||||
/** 已预留 */
|
||||
private Integer reserved;
|
||||
/** 已使用 */
|
||||
private Integer used;
|
||||
}
|
||||
Reference in New Issue
Block a user