UserTeamShareTask.java 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300
  1. package modules.user;
  2. import com.jfinal.plugin.activerecord.Db;
  3. import com.jfinal.plugin.activerecord.Record;
  4. import common.jfinal.AppConfig;
  5. import common.model.DepositLog;
  6. import common.model.Nftt;
  7. import common.model.Order;
  8. import common.model.User;
  9. import java.math.BigDecimal;
  10. import java.math.RoundingMode;
  11. import java.util.ArrayList;
  12. import java.util.List;
  13. import java.util.Map;
  14. import java.util.Set;
  15. import java.util.stream.Collectors;
  16. public class UserTeamShareTask implements Runnable {
  17. Order order;
  18. UserService userService;
  19. public UserTeamShareTask(Order order, UserService userService) {
  20. this.userService = userService;
  21. this.order = order;
  22. }
  23. // 统一分润逻辑,并进行容错和类型转换 (BigDecimal)
  24. private void processShare(String mobileNumber, BigDecimal percentage, String description) throws Exception {
  25. // 确保 order.getTotalPrice() 返回的是 BigDecimal,或者转换为 BigDecimal
  26. BigDecimal totalPrice = order.getBigDecimal("total_price");
  27. if (totalPrice == null) {
  28. throw new RuntimeException("订单总价为空,无法分润");
  29. }
  30. // 计算分润金额
  31. // 使用 BigDecimal 进行精确计算,避免浮点数精度问题
  32. BigDecimal shareAmount = totalPrice.multiply(percentage).divide(BigDecimal.valueOf(100), 4, RoundingMode.HALF_UP);
  33. // 查找用户 (userService 是注入的)
  34. User user = userService.findUserByMobileNumber(mobileNumber);
  35. if (user == null) {
  36. AppConfig.LOGGER.warn("分润用户 {} 不存在,无法分润", mobileNumber);
  37. // 根据业务需求抛出异常或跳过
  38. return;
  39. }
  40. // 更新用户余额
  41. BigDecimal currentBalance = user.getBigDecimal("balance");
  42. // 如果 balance 字段为 null,初始化为 0
  43. if (currentBalance == null) {
  44. currentBalance = BigDecimal.ZERO;
  45. }
  46. user.set("balance", currentBalance.add(shareAmount));
  47. if (!user.update()) {
  48. throw new RuntimeException("更新用户 " + user.getStr("nickname") + " 余额失败");
  49. }
  50. // 记录分润日志
  51. DepositLog log = new DepositLog();
  52. log.set("create_time", System.currentTimeMillis());
  53. log.set("is_deleted", 0);
  54. log.set("description", description + " " + percentage.intValue() + "%"); // 描述中带上百分比
  55. log.set("amount", shareAmount);
  56. log.set("user_id", user.getLong("id"));
  57. if (!log.save()) {
  58. throw new RuntimeException("保存分润日志失败 for user " + user.getStr("nickname"));
  59. }
  60. AppConfig.LOGGER.info("{} {}: {}", description, user.getStr("nickname"), shareAmount);
  61. }
  62. /**
  63. * 第一类、单独账号分润
  64. * 0. 大领导 2%
  65. * 1. 团队奖励 5%
  66. * 2. 市场预留 3%
  67. * 4. 宣传外联 4%
  68. * 4. 技术公司运维 1%
  69. * 5. 平台费用 1%
  70. */
  71. public void class1() throws Exception {
  72. AppConfig.LOGGER.info("正在处理订单 {} 的第一类分润", order.getLong("id"));
  73. // 确保这些手机号是常量,或者从配置中读取
  74. processShare("27788881000", BigDecimal.valueOf(2), "大领导分润");
  75. processShare("27788881001", BigDecimal.valueOf(5), "团队奖励");
  76. processShare("27788881002", BigDecimal.valueOf(3), "市场预留");
  77. processShare("27788881003", BigDecimal.valueOf(4), "宣传外联");
  78. // 注意这里是 1%,用 BigDecimal.valueOf(1)
  79. processShare("27788881004", BigDecimal.valueOf(1), "技术公司运维");
  80. processShare("27788881005", BigDecimal.valueOf(1), "平台费用");
  81. AppConfig.LOGGER.info("订单 {} 第一类分润处理完成", order.getLong("id"));
  82. }
  83. /**
  84. * 第二类、直推分润 10%
  85. */
  86. public void class2() {
  87. User user = User.dao.findById(order.getUserId());
  88. User referrerUser = User.dao.findById(user.getReferrerId());
  89. // 直推人不存在,直接跳过
  90. if (referrerUser == null) {
  91. return;
  92. }
  93. // 确保 order.getTotalPrice() 返回的是 BigDecimal,或者转换为 BigDecimal
  94. BigDecimal totalPrice = order.getBigDecimal("total_price");
  95. BigDecimal percentage = BigDecimal.valueOf(10);
  96. if (totalPrice == null) {
  97. throw new RuntimeException("订单总价为空,无法分润");
  98. }
  99. // 直推10%分润
  100. BigDecimal shareAmount = totalPrice.multiply(percentage).divide(BigDecimal.valueOf(100), 4, RoundingMode.HALF_UP);
  101. // 更新用户余额
  102. BigDecimal currentBalance = referrerUser.getBigDecimal("balance");
  103. // 如果 balance 字段为 null,初始化为 0
  104. if (currentBalance == null) {
  105. currentBalance = BigDecimal.ZERO;
  106. }
  107. referrerUser.set("balance", currentBalance.add(shareAmount));
  108. if (!referrerUser.update()) {
  109. throw new RuntimeException("更新用户 " + referrerUser.getMobileNumber() + " 余额失败");
  110. }
  111. // 记录分润日志
  112. String description = "直推分润";
  113. DepositLog log = new DepositLog();
  114. log.set("create_time", System.currentTimeMillis());
  115. log.set("is_deleted", 0);
  116. log.set("description", description + " " + percentage.intValue() + "%"); // 描述中带上百分比
  117. log.set("amount", shareAmount);
  118. log.set("user_id", referrerUser.getLong("id"));
  119. if (!log.save()) {
  120. throw new RuntimeException("保存分润日志失败 for user " + referrerUser.getMobileNumber());
  121. }
  122. AppConfig.LOGGER.info("{} {}: {}", description, referrerUser.getMobileNumber(), shareAmount);
  123. }
  124. /**
  125. * 第三类、NFT持有者分润
  126. * 1. 10%的部分
  127. * 2. 4%的部分
  128. */
  129. public void class3() {
  130. String queryUserIdSql = "SELECT user_id, COUNT(id) AS valid_order_count " +
  131. "FROM t_order " +
  132. "WHERE order_status = 60 AND delivery_status = 1 " +
  133. "GROUP BY user_id HAVING COUNT(id) > 0";
  134. List<Record> userOrderCounts = Db.find(queryUserIdSql);
  135. // 提取所有唯一的 user_id,用于下一步批量查询 User 对象
  136. Set<Long> eligibleUserIds = userOrderCounts.stream()
  137. .map(record -> record.getLong("user_id"))
  138. .collect(Collectors.toSet());
  139. // 如果没有符合条件的用户,直接返回
  140. if (eligibleUserIds.isEmpty()) {
  141. AppConfig.LOGGER.info("没有符合分润条件的用户。");
  142. return;
  143. }
  144. // 第二步:根据这些 user_id,批量查询出对应的 User 对象
  145. // JFinal 的 IN 操作比较方便,但是当列表过大时,分批查询 User 仍然是最佳实践
  146. // 假设 eligibleUserIds 不会过大,可以直接查询
  147. String inSql = eligibleUserIds.stream()
  148. .map(String::valueOf)
  149. .collect(Collectors.joining(","));
  150. AppConfig.LOGGER.info("开始查询用户,符合条件的用户ID数量: {}", eligibleUserIds.size());
  151. // ******** 关键改进:分批查询 User 对象 ********
  152. final int BATCH_QUERY_SIZE = 1000; // 每批次查询 1000 个用户
  153. List<User> userList = new ArrayList<>(eligibleUserIds.size()); // 预估大小,减少扩容开销
  154. // 将 Set 转换为 List,方便进行 subList 分批操作
  155. List<Long> eligibleUserIdsList = new ArrayList<>(eligibleUserIds);
  156. for (int i = 0; i < eligibleUserIdsList.size(); i += BATCH_QUERY_SIZE) {
  157. // 获取当前批次的 user_id 子列表
  158. List<Long> subList = eligibleUserIdsList.subList(i, Math.min(i + BATCH_QUERY_SIZE, eligibleUserIdsList.size()));
  159. String subInSql = subList.stream()
  160. .map(String::valueOf)
  161. .collect(Collectors.joining(","));
  162. // 执行 IN 查询,添加到总的 userList 中
  163. userList.addAll(User.dao.find("SELECT id, balance FROM t_user WHERE id IN (" + subInSql + ")"));
  164. AppConfig.LOGGER.debug("已查询用户批次 {} 到 {},总已查询用户数: {}", i, Math.min(i + BATCH_QUERY_SIZE, eligibleUserIdsList.size()) -1 , userList.size());
  165. }
  166. // ******** 分批查询 User 对象结束 ********
  167. AppConfig.LOGGER.info("用户查询完毕,共查询到 {} 个符合用户。", userList.size());
  168. // 为了方便处理,将 list 转换为 map,userOrderCounts 也转换为 map
  169. Map<Long, User> userMap = userList.stream()
  170. .collect(Collectors.toMap(User::getId, user -> user));
  171. Map<Long, Integer> userValidOrderCountMap = userOrderCounts.stream()
  172. .collect(Collectors.toMap(
  173. record -> record.getLong("user_id"),
  174. record -> record.getLong("valid_order_count").intValue()
  175. ));
  176. // 现在,你可以遍历 userOrderCounts,结合 userMap 进行分润处理
  177. List<User> usersToUpdate = new ArrayList<>();
  178. List<DepositLog> depositLogsToSave = new ArrayList<>();
  179. // 假设分润规则是:每个用户获得的总利润 = (该用户的有效订单数量 / 所有用户的总有效订单数量) * 总利润
  180. // 或者:简单地每个用户的固定份额
  181. // 这里我们用总订单数量来计算每个用户的分润比例
  182. long totalValidOrders = userValidOrderCountMap.values().stream().mapToLong(Integer::longValue).sum();
  183. // 避免除以零
  184. if (totalValidOrders == 0) {
  185. AppConfig.LOGGER.warn("所有符合条件用户的总订单数为0,无法按订单比例分润。");
  186. return;
  187. }
  188. // 总价值
  189. BigDecimal totalPrice = BigDecimal.valueOf(order.getTotalPrice());
  190. Nftt nftt = Nftt.dao.findById(10000);
  191. for (Record record : userOrderCounts) {
  192. // 参与分润的用户
  193. Long userId = record.getLong("user_id");
  194. // 当前用户的有效订单数量
  195. int validOrderCount = record.getLong("valid_order_count").intValue();
  196. User user = userMap.get(userId);
  197. if (user == null) {
  198. AppConfig.LOGGER.warn("未找到用户ID为 {} 的User对象,跳过分润。", userId);
  199. continue;
  200. }
  201. // 分润1,其中4%作为NFT持有者分享(0.04 * validOrderCount / totalValidOrders)
  202. BigDecimal shareArtio1 = BigDecimal.valueOf(0.04);
  203. BigDecimal userShareRatio1 = shareArtio1.multiply(BigDecimal.valueOf(validOrderCount))
  204. .divide(BigDecimal.valueOf(totalValidOrders), 8, RoundingMode.HALF_UP);// 确保高精度
  205. BigDecimal shareAmount1 = totalPrice.multiply(userShareRatio1).setScale(2, RoundingMode.HALF_UP); // 四舍五入到两位小数
  206. // 分润2,NFT持有者共享总数的10%,还没售卖出去的部分归平台所有 (0.1 * validOrderCount / Nftt.maxQuantity)
  207. BigDecimal shareArtio2 = BigDecimal.valueOf(0.1);
  208. BigDecimal userShareRatio2 = shareArtio2.multiply(BigDecimal.valueOf(validOrderCount))
  209. .divide(BigDecimal.valueOf(nftt.getMaxQuantity()), 8, RoundingMode.HALF_UP);// 确保高精度,注意这里的底数是MaxQuantity
  210. BigDecimal shareAmount2 = totalPrice.multiply(userShareRatio2).setScale(2, RoundingMode.HALF_UP); // 四舍五入到两位小数
  211. // 更新用户余额
  212. BigDecimal currentBalance = user.getBigDecimal("balance");
  213. if (currentBalance == null) {
  214. currentBalance = BigDecimal.ZERO;
  215. }
  216. user.set("balance", currentBalance.add(shareAmount1).add(shareAmount2).floatValue());
  217. usersToUpdate.add(user);
  218. // 创建分润日志
  219. DepositLog log1 = new DepositLog();
  220. log1.set("create_time", System.currentTimeMillis());
  221. log1.set("is_deleted", 0);
  222. log1.set("description", "4%作为NFT持有者分享," + validOrderCount + " / " + totalValidOrders); // 加入订单数量信息
  223. log1.set("amount", shareAmount1);
  224. log1.set("user_id", userId);
  225. depositLogsToSave.add(log1);
  226. DepositLog log2 = new DepositLog();
  227. log2.set("create_time", System.currentTimeMillis());
  228. log2.set("is_deleted", 0);
  229. log2.set("description", "NFT持有者共享总数的10%," + validOrderCount + " / " + nftt.getMaxQuantity()); // 加入订单数量信息
  230. log2.set("amount", shareAmount2);
  231. log2.set("user_id", userId);
  232. depositLogsToSave.add(log2);
  233. }
  234. // 在一个事务中执行所有批量更新和插入
  235. Db.tx(() -> {
  236. int[] userUpdateResult = Db.batchUpdate(usersToUpdate, 1000); // 批量更新用户
  237. int[] logSaveResult = Db.batchSave(depositLogsToSave, 1000); // 批量保存日志
  238. AppConfig.LOGGER.info("已批量更新 {} 个用户余额,批量保存 {} 条分润日志。", userUpdateResult.length, logSaveResult.length);
  239. return true; // 事务成功
  240. });
  241. }
  242. /**
  243. * 第四类、持有者激励分润,这个最复杂最后做,可能需要为user加一个时间戳字段
  244. */
  245. public void class4() {}
  246. @Override
  247. public void run() {
  248. // 确保整个分润过程在一个数据库事务中
  249. // 因为操作涉及到多个 User 和 DepositLog 的更新和插入
  250. Db.tx(() -> {
  251. try {
  252. this.class1();
  253. this.class2();
  254. this.class3();
  255. this.class4();
  256. return true; // 事务成功
  257. } catch (Exception e) {
  258. AppConfig.LOGGER.error("订单分润处理异常,订单ID: {}, 错误: {}", order.getLong("id"), e.getMessage(), e);
  259. return false; // 事务失败,回滚
  260. }
  261. });
  262. }
  263. }