初始化天卡开放平台后端项目

包含完整功能模块:
- 设备激活(直销激活码模式 + 代销免激活码模式)
- 微信支付 + 天卡订单
- 天卡池管理 + 异步橘视厅SDK激活
- 第三方合作伙伴接口(代销模式)
  - 签名鉴权(appKey/appSecret + nonce防重放)
  - 天卡消耗(事务 + 幂等 + 并发控制)
  - 媒资接口(三级授权过滤)
  - 审计日志
- 定价策略(3层:门店 > 渠道 > 平台)
- 分账(微信分账 + 分润规则)
- Docker部署(含Redis容器)
This commit is contained in:
zhaogd
2026-07-26 12:34:13 +08:00
commit 30198c7191
349 changed files with 28837 additions and 0 deletions

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}