From 4ae8f4d271c960d9347e18c385557b98da8b15a4 Mon Sep 17 00:00:00 2001 From: wangfukang <15630117759@163.com> Date: Thu, 29 Oct 2026 13:48:24 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AF=B9=E6=8E=A5=E6=8B=BC=E5=9B=A2=E6=95=B0?= =?UTF-8?q?=E6=8D=AE1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/resources/application.yml | 15 +- hiver-admin/test-output/test-report.html | 16 +- .../main/java/cc/hiver/core/entity/User.java | 30 ++ .../main/java/cc/hiver/core/vo/WechatVo.java | 6 + .../controller/manage/UserController.java | 11 +- .../controller/InviteRewardController.java | 121 ++++++ .../mall/controller/WechatPayController.java | 9 + .../invite/entity/InviteRewardConfig.java | 57 +++ .../invite/entity/InviteRewardRecord.java | 69 ++++ .../mapper/InviteRewardConfigMapper.java | 9 + .../mapper/InviteRewardRecordMapper.java | 9 + .../invite/service/InviteRewardResult.java | 42 ++ .../invite/service/InviteRewardService.java | 33 ++ .../service/WechatMerchantTransferResult.java | 43 ++ .../WechatMerchantTransferService.java | 275 +++++++++++++ .../WechatTransferAuthorizationResult.java | 34 ++ .../service/impl/InviteRewardServiceImpl.java | 380 ++++++++++++++++++ .../controller/PlanetAdminController.java | 44 ++ .../cc/hiver/mall/utils/WechatPaySigner.java | 5 +- .../src/main/resources/db/invite_reward.sql | 48 +++ .../social/controller/WechatController.java | 35 +- 21 files changed, 1277 insertions(+), 14 deletions(-) create mode 100644 hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/InviteRewardController.java create mode 100644 hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/invite/entity/InviteRewardConfig.java create mode 100644 hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/invite/entity/InviteRewardRecord.java create mode 100644 hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/invite/mapper/InviteRewardConfigMapper.java create mode 100644 hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/invite/mapper/InviteRewardRecordMapper.java create mode 100644 hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/invite/service/InviteRewardResult.java create mode 100644 hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/invite/service/InviteRewardService.java create mode 100644 hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/invite/service/WechatMerchantTransferResult.java create mode 100644 hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/invite/service/WechatMerchantTransferService.java create mode 100644 hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/invite/service/WechatTransferAuthorizationResult.java create mode 100644 hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/invite/service/impl/InviteRewardServiceImpl.java create mode 100644 hiver-modules/hiver-mall/src/main/resources/db/invite_reward.sql diff --git a/hiver-admin/src/main/resources/application.yml b/hiver-admin/src/main/resources/application.yml index 601e4fb2..a96ad229 100644 --- a/hiver-admin/src/main/resources/application.yml +++ b/hiver-admin/src/main/resources/application.yml @@ -589,4 +589,17 @@ wechatpay: merchantSerialNo: 4B42A5EDF9CA2FC2ACEB21271A773A4F15B432A4 privateKeyPath: classpath:apiclient_key.pem platformCertificatePath: classpath:apiclient_cert.pem - notifyUrl: https://hbkuaishi.com/hiver/api/wechat/pay/notify \ No newline at end of file + notifyUrl: https://hbkuaishi.com/hiver/api/wechat/pay/notify + transferUrl: https://api.mch.weixin.qq.com/v3/fund-app/mch-transfer/transfer-bills + transferSceneId: 1000 + transferRemark: 邀请好友注册奖励 + transferUserRecvPerception: 现金奖励 + transferAuthorizationUrl: https://api.mch.weixin.qq.com/v3/fund-app/mch-transfer/user-confirm-authorization + transferAuthorizationQueryUrl: https://api.mch.weixin.qq.com/v3/fund-app/mch-transfer/user-confirm-authorization/out-authorization-no + transferAuthorizedTransferUrl: https://api.mch.weixin.qq.com/v3/fund-app/mch-transfer/transfer-bills/transfer + transferAuthorizationNotifyUrl: https://hbkuaishi.com/hiver/api/wechat/pay/transfer-auth-notify + transferSceneReportInfoType1: 活动名称 + transferSceneReportInfoContent1: 邀请好友注册奖励 + transferSceneReportInfoType2: 奖励说明 + transferSceneReportInfoContent2: 好友完成注册并绑定手机号奖励 + transferNotifyUrl: \ No newline at end of file diff --git a/hiver-admin/test-output/test-report.html b/hiver-admin/test-output/test-report.html index bc1699d2..fcdf9fb6 100644 --- a/hiver-admin/test-output/test-report.html +++ b/hiver-admin/test-output/test-report.html @@ -35,7 +35,7 @@ Hiver
  • -ʮÔÂ 28, 2026 10:46:04 +ʮÔÂ 29, 2026 13:09:25
  • @@ -84,7 +84,7 @@

    passTest

    -

    10:46:04 ÉÏÎç / 0.022 secs

    +

    13:09:26 ÏÂÎç / 0.023 secs

    @@ -92,9 +92,9 @@
    #test-id=1
    passTest
    -10.28.2026 10:46:04 -10.28.2026 10:46:04 -0.022 secs +10.29.2026 13:09:26 +10.29.2026 13:09:26 +0.023 secs
    @@ -104,7 +104,7 @@ Pass - 10:46:04 + 13:09:26 Test passed @@ -128,13 +128,13 @@

    Started

    -

    ʮÔÂ 28, 2026 10:46:04

    +

    ʮÔÂ 29, 2026 13:09:25

    Ended

    -

    ʮÔÂ 28, 2026 10:46:05

    +

    ʮÔÂ 29, 2026 13:09:26

    diff --git a/hiver-core/src/main/java/cc/hiver/core/entity/User.java b/hiver-core/src/main/java/cc/hiver/core/entity/User.java index a6d11cb2..6972be58 100644 --- a/hiver-core/src/main/java/cc/hiver/core/entity/User.java +++ b/hiver-core/src/main/java/cc/hiver/core/entity/User.java @@ -169,6 +169,36 @@ public class User extends HiverBaseEntity { @ApiModelProperty(value = "邀请人Id") private String inviterId; + @ApiModelProperty(value = "拉新来源区域id") + private String inviteRegionId; + + @ApiModelProperty(value = "拉新设备标识") + private String inviteDeviceId; + + @ApiModelProperty(value = "是否邀请链路新用户 1是 0否") + private Integer inviteNewUser = 0; + + @ApiModelProperty(value = "拉新奖励状态 0未处理 1成功 2失败 3跳过") + private Integer inviteRewardStatus = 0; + + @ApiModelProperty(value = "拉新奖励金额") + private BigDecimal inviteRewardAmount = new BigDecimal(0); + + @ApiModelProperty(value = "拉新奖励时间") + private Date inviteRewardTime; + + @ApiModelProperty(value = "拉新转账免确认授权商户单号") + private String inviteTransferAuthNo; + + @ApiModelProperty(value = "拉新转账微信免确认授权单号") + private String inviteTransferAuthId; + + @ApiModelProperty(value = "拉新转账免确认授权状态") + private String inviteTransferAuthState; + + @ApiModelProperty(value = "拉新转账免确认授权时间") + private Date inviteTransferAuthTime; + @ApiModelProperty("当前账户余额") private BigDecimal depoBal = new BigDecimal(0); diff --git a/hiver-core/src/main/java/cc/hiver/core/vo/WechatVo.java b/hiver-core/src/main/java/cc/hiver/core/vo/WechatVo.java index 3677d9cf..d42c22f8 100644 --- a/hiver-core/src/main/java/cc/hiver/core/vo/WechatVo.java +++ b/hiver-core/src/main/java/cc/hiver/core/vo/WechatVo.java @@ -17,6 +17,12 @@ public class WechatVo { @ApiModelProperty(value = "inviterId") private String inviterId; + @ApiModelProperty(value = "拉新来源区域id") + private String regionId; + + @ApiModelProperty(value = "拉新设备标识") + private String deviceId; + @ApiModelProperty(value = "wechatName") private String wechatName; diff --git a/hiver-modules/hiver-base/src/main/java/cc/hiver/base/controller/manage/UserController.java b/hiver-modules/hiver-base/src/main/java/cc/hiver/base/controller/manage/UserController.java index 705bb333..44a7e064 100644 --- a/hiver-modules/hiver-base/src/main/java/cc/hiver/base/controller/manage/UserController.java +++ b/hiver-modules/hiver-base/src/main/java/cc/hiver/base/controller/manage/UserController.java @@ -690,6 +690,7 @@ public class UserController { } if (wechatUser == null) { + boolean newUserCreated = false; log.info("微信登录未根据userid和unionid查询到用户,准备新增用户userId:{};unionid:{}",wechatVo.getUserId(),unionid); try { // 新增用户信息 @@ -704,10 +705,15 @@ public class UserController { wechatUser.setPassword(encryptPass); wechatUser.setType(UserConstant.USER_TYPE_NORMAL); //设置邀请人 - if(wechatVo.getInviterId() != null){ + if(StringUtils.isNotEmpty(wechatVo.getInviterId())){ wechatUser.setInviterId(wechatVo.getInviterId()); + wechatUser.setInviteRegionId(wechatVo.getRegionId()); + wechatUser.setInviteDeviceId(wechatVo.getDeviceId()); + wechatUser.setInviteNewUser(1); + wechatUser.setInviteRewardStatus(0); } wechatUser = userService.save(wechatUser); + newUserCreated = true; // 设置用户角色 // 默认角色 final List roleList = roleService.findByDefaultRole(true); @@ -725,10 +731,13 @@ public class UserController { //customerService.updateUserId(wechatVo.getCustomId(), wechatUser.getId()); // 返回当前登录人的信息 resultMap.put("msg", "新增用户信息,并绑定成功!"); + resultMap.put("isNewUser", newUserCreated); //resultMap.put("customId", wechatVo.getCustomId()); } catch (Exception e) { return ResultUtil.error(e.getMessage()); } + } else { + resultMap.put("isNewUser", false); } wechatUser.setUnionid(unionid); wechatUser.setWechatName(wechatVo.getWechatName()); diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/InviteRewardController.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/InviteRewardController.java new file mode 100644 index 00000000..9d880453 --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/InviteRewardController.java @@ -0,0 +1,121 @@ +package cc.hiver.mall.controller; + +import cc.hiver.core.common.utils.ResultUtil; +import cc.hiver.core.common.utils.SecurityUtil; +import cc.hiver.core.common.vo.PageVo; +import cc.hiver.core.common.vo.Result; +import cc.hiver.core.entity.User; +import cc.hiver.mall.invite.entity.InviteRewardConfig; +import cc.hiver.mall.invite.entity.InviteRewardRecord; +import cc.hiver.mall.invite.service.InviteRewardService; +import cc.hiver.mall.invite.service.WechatTransferAuthorizationResult; +import cc.hiver.mall.utils.WechatPayConfig; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.HashMap; +import java.util.Map; + +@RestController +@Api(tags = "拉新返现接口") +@RequestMapping("/hiver/app/inviteReward") +public class InviteRewardController { + + @Autowired + private SecurityUtil securityUtil; + + @Autowired + private InviteRewardService inviteRewardService; + + @Autowired + private WechatPayConfig wechatPayConfig; + + @Value("${wechatpay.mchId:}") + private String mchId; + + private PageVo buildPage(Integer pageNumber, Integer pageSize) { + PageVo pageVo = new PageVo(); + pageVo.setPageNumber(pageNumber == null ? 1 : pageNumber); + pageVo.setPageSize(pageSize == null ? 10 : pageSize); + return pageVo; + } + + @ApiOperation(value = "我的拉新返现记录") + @RequestMapping(value = "/my/page", method = RequestMethod.GET) + public Result> myPage(@RequestParam(required = false) Integer pageNumber, + @RequestParam(required = false) Integer pageSize) { + User user = securityUtil.getCurrUser(); + if (user == null || user.getId() == null) { + return ResultUtil.error("请先登录"); + } + return new ResultUtil>() + .setData(inviteRewardService.pageRecords(buildPage(pageNumber, pageSize), null, user.getId())); + } + + @ApiOperation(value = "当前区域拉新返现配置") + @RequestMapping(value = "/config", method = RequestMethod.GET) + public Result config(@RequestParam(required = false) String regionId) { + return new ResultUtil().setData(inviteRewardService.getEnabledConfig(regionId)); + } + + @ApiOperation(value = "免确认收款授权状态") + @RequestMapping(value = "/transferAuth/status", method = RequestMethod.GET) + public Result> transferAuthStatus() { + User user = securityUtil.getCurrUser(); + if (user == null || user.getId() == null) { + return ResultUtil.error("请先登录"); + } + WechatTransferAuthorizationResult result = inviteRewardService.refreshTransferAuthorization(user, false); + if (result.isSuccess()) { + return new ResultUtil>().setData(buildAuthData(user, result)); + } + Map data = buildAuthData(user, null); + return new ResultUtil>().setData(data); + } + + @ApiOperation(value = "申请免确认收款授权") + @RequestMapping(value = "/transferAuth/apply", method = RequestMethod.POST) + public Result> applyTransferAuth() { + User user = securityUtil.getCurrUser(); + if (user == null || user.getId() == null) { + return ResultUtil.error("请先登录"); + } + WechatTransferAuthorizationResult result = inviteRewardService.applyTransferAuthorization(user); + if (!result.isSuccess()) { + return ResultUtil.error(result.getMessage()); + } + return new ResultUtil>().setData(buildAuthData(user, result)); + } + + @ApiOperation(value = "刷新免确认收款授权状态") + @RequestMapping(value = "/transferAuth/query", method = RequestMethod.POST) + public Result> queryTransferAuth() { + User user = securityUtil.getCurrUser(); + if (user == null || user.getId() == null) { + return ResultUtil.error("请先登录"); + } + WechatTransferAuthorizationResult result = inviteRewardService.refreshTransferAuthorization(user, true); + if (!result.isSuccess()) { + return ResultUtil.error(result.getMessage()); + } + return new ResultUtil>().setData(buildAuthData(user, result)); + } + + private Map buildAuthData(User user, WechatTransferAuthorizationResult result) { + Map data = new HashMap<>(); + data.put("mchId", mchId); + data.put("appId", wechatPayConfig.getAppId()); + data.put("authNo", result == null ? user.getInviteTransferAuthNo() : result.getOutAuthorizationNo()); + data.put("authId", result == null ? user.getInviteTransferAuthId() : result.getAuthorizationId()); + data.put("state", result == null ? user.getInviteTransferAuthState() : result.getState()); + data.put("packageInfo", result == null ? "" : result.getPackageInfo()); + return data; + } +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/WechatPayController.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/WechatPayController.java index ec8fe6b2..45bde4ad 100644 --- a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/WechatPayController.java +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/WechatPayController.java @@ -209,6 +209,15 @@ public class WechatPayController { } } + @PostMapping("/transfer-auth-notify") + public ResponseEntity> transferAuthNotify(@RequestBody Map request) { + log.info("接收到微信商家转账免确认授权回调:{}", request); + Map result = new HashMap<>(); + result.put("code", "SUCCESS"); + result.put("message", "成功"); + return ResponseEntity.ok(result); + } + public Map buildPaySign(String prepayId) throws Exception { // 1. 生成参数 String timeStamp = String.valueOf(System.currentTimeMillis() / 1000); // 秒 diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/invite/entity/InviteRewardConfig.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/invite/entity/InviteRewardConfig.java new file mode 100644 index 00000000..b3ddf7e4 --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/invite/entity/InviteRewardConfig.java @@ -0,0 +1,57 @@ +package cc.hiver.mall.invite.entity; + +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import javax.persistence.Id; +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; + +@Data +@ApiModel(value = "拉新返现配置") +@TableName(value = "t_invite_reward_config", autoResultMap = true) +public class InviteRewardConfig implements Serializable { + private static final long serialVersionUID = 1L; + + @Id + @TableId + @ApiModelProperty(value = "主键") + private String id; + + @ApiModelProperty(value = "区域id") + private String regionId; + + @ApiModelProperty(value = "每个有效新用户奖励金额") + private BigDecimal rewardAmount; + + @ApiModelProperty(value = "每日发放上限 0不限") + private Integer dailyLimit; + + @ApiModelProperty(value = "总预算 0不限") + private BigDecimal totalBudget; + + @ApiModelProperty(value = "已发放预算") + private BigDecimal usedBudget; + + @ApiModelProperty(value = "是否启用 1是 0否") + private Integer enabled; + + @ApiModelProperty(value = "备注") + private String remark; + + @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @ApiModelProperty(value = "创建时间") + private Date createTime; + + @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @ApiModelProperty(value = "更新时间") + private Date updateTime; +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/invite/entity/InviteRewardRecord.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/invite/entity/InviteRewardRecord.java new file mode 100644 index 00000000..02caaf4c --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/invite/entity/InviteRewardRecord.java @@ -0,0 +1,69 @@ +package cc.hiver.mall.invite.entity; + +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import javax.persistence.Id; +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; + +@Data +@ApiModel(value = "拉新返现奖励记录") +@TableName(value = "t_invite_reward_record", autoResultMap = true) +public class InviteRewardRecord implements Serializable { + private static final long serialVersionUID = 1L; + + @Id + @TableId + @ApiModelProperty(value = "主键") + private String id; + + @ApiModelProperty(value = "邀请人用户id") + private String inviterId; + + @ApiModelProperty(value = "被邀请人用户id") + private String invitedUserId; + + @ApiModelProperty(value = "被邀请人手机号") + private String invitedMobile; + + @ApiModelProperty(value = "邀请设备标识") + private String inviteDeviceId; + + @ApiModelProperty(value = "发生奖励时的区域id") + private String regionId; + + @ApiModelProperty(value = "奖励金额") + private BigDecimal amount; + + @ApiModelProperty(value = "状态 0处理中 1成功 2失败 3跳过") + private Integer status; + + @ApiModelProperty(value = "微信商家转账商户单号") + private String outBillNo; + + @ApiModelProperty(value = "微信商家转账单号") + private String transferBillNo; + + @ApiModelProperty(value = "失败或跳过原因") + private String failReason; + + @ApiModelProperty(value = "微信响应") + private String wxResponse; + + @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @ApiModelProperty(value = "创建时间") + private Date createTime; + + @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @ApiModelProperty(value = "更新时间") + private Date updateTime; +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/invite/mapper/InviteRewardConfigMapper.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/invite/mapper/InviteRewardConfigMapper.java new file mode 100644 index 00000000..6ab817e5 --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/invite/mapper/InviteRewardConfigMapper.java @@ -0,0 +1,9 @@ +package cc.hiver.mall.invite.mapper; + +import cc.hiver.mall.invite.entity.InviteRewardConfig; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.springframework.stereotype.Repository; + +@Repository +public interface InviteRewardConfigMapper extends BaseMapper { +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/invite/mapper/InviteRewardRecordMapper.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/invite/mapper/InviteRewardRecordMapper.java new file mode 100644 index 00000000..ab799f05 --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/invite/mapper/InviteRewardRecordMapper.java @@ -0,0 +1,9 @@ +package cc.hiver.mall.invite.mapper; + +import cc.hiver.mall.invite.entity.InviteRewardRecord; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.springframework.stereotype.Repository; + +@Repository +public interface InviteRewardRecordMapper extends BaseMapper { +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/invite/service/InviteRewardResult.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/invite/service/InviteRewardResult.java new file mode 100644 index 00000000..46d684c1 --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/invite/service/InviteRewardResult.java @@ -0,0 +1,42 @@ +package cc.hiver.mall.invite.service; + +import lombok.Data; + +import java.math.BigDecimal; + +@Data +public class InviteRewardResult { + private boolean rewarded; + private boolean skipped; + private boolean processing; + private BigDecimal amount; + private String message; + + public static InviteRewardResult rewarded(BigDecimal amount, String message) { + InviteRewardResult result = new InviteRewardResult(); + result.setRewarded(true); + result.setAmount(amount); + result.setMessage(message); + return result; + } + + public static InviteRewardResult skipped(String message) { + InviteRewardResult result = new InviteRewardResult(); + result.setSkipped(true); + result.setMessage(message); + return result; + } + + public static InviteRewardResult processing(String message) { + InviteRewardResult result = new InviteRewardResult(); + result.setProcessing(true); + result.setMessage(message); + return result; + } + + public static InviteRewardResult failed(String message) { + InviteRewardResult result = new InviteRewardResult(); + result.setMessage(message); + return result; + } +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/invite/service/InviteRewardService.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/invite/service/InviteRewardService.java new file mode 100644 index 00000000..5615225c --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/invite/service/InviteRewardService.java @@ -0,0 +1,33 @@ +package cc.hiver.mall.invite.service; + +import cc.hiver.core.common.vo.PageVo; +import cc.hiver.core.entity.User; +import cc.hiver.mall.invite.entity.InviteRewardConfig; +import cc.hiver.mall.invite.entity.InviteRewardRecord; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; + +public interface InviteRewardService { + + Page pageConfigs(PageVo pageVo, String regionId); + + Page pageRecords(PageVo pageVo, String regionId, String inviterId); + + InviteRewardConfig getEnabledConfig(String regionId); + + InviteRewardConfig saveConfig(InviteRewardConfig config); + + void deleteConfig(String id); + + WechatTransferAuthorizationResult applyTransferAuthorization(User user); + + WechatTransferAuthorizationResult refreshTransferAuthorization(User user, boolean displayAuthorization); + + InviteRewardResult rewardAfterPhoneBound(User invitedUser, + String phoneNumber, + String inviterId, + String regionId, + String deviceId, + Double lat, + Double lng, + boolean firstPhoneBind); +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/invite/service/WechatMerchantTransferResult.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/invite/service/WechatMerchantTransferResult.java new file mode 100644 index 00000000..4e0e26bd --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/invite/service/WechatMerchantTransferResult.java @@ -0,0 +1,43 @@ +package cc.hiver.mall.invite.service; + +import lombok.Data; + +@Data +public class WechatMerchantTransferResult { + private boolean success; + private boolean processing; + private String state; + private String transferBillNo; + private String packageInfo; + private String responseBody; + private String message; + + public static WechatMerchantTransferResult success(String state, String transferBillNo, String packageInfo, String responseBody) { + WechatMerchantTransferResult result = new WechatMerchantTransferResult(); + result.setSuccess(true); + result.setState(state); + result.setTransferBillNo(transferBillNo); + result.setPackageInfo(packageInfo); + result.setResponseBody(responseBody); + return result; + } + + public static WechatMerchantTransferResult processing(String state, String transferBillNo, String packageInfo, String message, String responseBody) { + WechatMerchantTransferResult result = new WechatMerchantTransferResult(); + result.setProcessing(true); + result.setState(state); + result.setTransferBillNo(transferBillNo); + result.setPackageInfo(packageInfo); + result.setMessage(message); + result.setResponseBody(responseBody); + return result; + } + + public static WechatMerchantTransferResult fail(String message, String responseBody) { + WechatMerchantTransferResult result = new WechatMerchantTransferResult(); + result.setSuccess(false); + result.setMessage(message); + result.setResponseBody(responseBody); + return result; + } +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/invite/service/WechatMerchantTransferService.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/invite/service/WechatMerchantTransferService.java new file mode 100644 index 00000000..454aae03 --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/invite/service/WechatMerchantTransferService.java @@ -0,0 +1,275 @@ +package cc.hiver.mall.invite.service; + +import cc.hiver.mall.utils.WechatPayConfig; +import cc.hiver.mall.utils.WechatPaySigner; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import okhttp3.*; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Service +public class WechatMerchantTransferService { + + @Value("${wechatpay.transferUrl:https://api.mch.weixin.qq.com/v3/fund-app/mch-transfer/transfer-bills}") + private String transferUrl; + + @Value("${wechatpay.transferSceneId:1000}") + private String transferSceneId; + + @Value("${wechatpay.transferRemark:邀请好友注册奖励}") + private String transferRemark; + + @Value("${wechatpay.transferUserRecvPerception:现金奖励}") + private String userRecvPerception; + + @Value("${wechatpay.transferNotifyUrl:}") + private String transferNotifyUrl; + + @Value("${wechatpay.transferAuthorizationUrl:https://api.mch.weixin.qq.com/v3/fund-app/mch-transfer/user-confirm-authorization}") + private String transferAuthorizationUrl; + + @Value("${wechatpay.transferAuthorizationQueryUrl:https://api.mch.weixin.qq.com/v3/fund-app/mch-transfer/user-confirm-authorization/out-authorization-no}") + private String transferAuthorizationQueryUrl; + + @Value("${wechatpay.transferAuthorizedTransferUrl:https://api.mch.weixin.qq.com/v3/fund-app/mch-transfer/transfer-bills/transfer}") + private String transferAuthorizedTransferUrl; + + @Value("${wechatpay.transferAuthorizationNotifyUrl:}") + private String transferAuthorizationNotifyUrl; + + @Value("${wechatpay.transferSceneReportInfoType1:活动名称}") + private String transferSceneReportInfoType1; + + @Value("${wechatpay.transferSceneReportInfoContent1:邀请好友注册奖励}") + private String transferSceneReportInfoContent1; + + @Value("${wechatpay.transferSceneReportInfoType2:奖励说明}") + private String transferSceneReportInfoType2; + + @Value("${wechatpay.transferSceneReportInfoContent2:好友完成注册并绑定手机号奖励}") + private String transferSceneReportInfoContent2; + + @Autowired + private WechatPayConfig config; + + @Autowired + private WechatPaySigner signer; + + private final OkHttpClient httpClient = new OkHttpClient(); + private final ObjectMapper objectMapper = new ObjectMapper(); + + public WechatTransferAuthorizationResult applyAuthorization(String openid, String displayName, String outAuthorizationNo) { + if (StringUtils.isBlank(openid)) { + return WechatTransferAuthorizationResult.fail("用户缺少小程序openid", ""); + } + if (StringUtils.isBlank(outAuthorizationNo)) { + return WechatTransferAuthorizationResult.fail("授权单号不能为空", ""); + } + if (StringUtils.isBlank(transferAuthorizationNotifyUrl)) { + return WechatTransferAuthorizationResult.fail("未配置免确认收款授权回调地址", ""); + } + try { + Map reqBody = new HashMap<>(); + reqBody.put("out_authorization_no", outAuthorizationNo); + reqBody.put("appid", config.getAppId()); + reqBody.put("openid", openid); + reqBody.put("transfer_scene_id", transferSceneId); + reqBody.put("user_display_name", StringUtils.defaultIfBlank(displayName, "用户")); + reqBody.put("user_recv_perception", userRecvPerception); + reqBody.put("authorization_notify_url", transferAuthorizationNotifyUrl); + + String jsonBody = objectMapper.writeValueAsString(reqBody); + Request request = newJsonRequest(transferAuthorizationUrl, jsonBody); + try (Response response = httpClient.newCall(request).execute()) { + String responseBody = response.body() == null ? "" : response.body().string(); + if (!response.isSuccessful()) { + return WechatTransferAuthorizationResult.fail("微信免确认收款授权申请失败: " + response.code(), responseBody); + } + JsonNode root = objectMapper.readTree(responseBody); + return WechatTransferAuthorizationResult.success( + root.path("out_authorization_no").asText(outAuthorizationNo), + root.path("authorization_id").asText(""), + root.path("state").asText(""), + root.path("package_info").asText(""), + responseBody + ); + } + } catch (Exception e) { + return WechatTransferAuthorizationResult.fail(e.getMessage(), ""); + } + } + + public WechatTransferAuthorizationResult queryAuthorization(String outAuthorizationNo, boolean displayAuthorization) { + if (StringUtils.isBlank(outAuthorizationNo)) { + return WechatTransferAuthorizationResult.fail("授权单号不能为空", ""); + } + try { + String url = transferAuthorizationQueryUrl + "/" + outAuthorizationNo + + "?is_display_authorization=" + displayAuthorization; + Request request = new Request.Builder() + .url(url) + .get() + .addHeader("Authorization", "WECHATPAY2-SHA256-RSA2048 " + signer.sign("GET", url, "")) + .addHeader("Accept", "application/json") + .addHeader("User-Agent", "Hiver/1.0") + .build(); + try (Response response = httpClient.newCall(request).execute()) { + String responseBody = response.body() == null ? "" : response.body().string(); + if (!response.isSuccessful()) { + return WechatTransferAuthorizationResult.fail("微信免确认收款授权查询失败: " + response.code(), responseBody); + } + JsonNode root = objectMapper.readTree(responseBody); + return WechatTransferAuthorizationResult.success( + root.path("out_authorization_no").asText(outAuthorizationNo), + root.path("authorization_id").asText(""), + root.path("state").asText(""), + root.path("package_info").asText(""), + responseBody + ); + } + } catch (Exception e) { + return WechatTransferAuthorizationResult.fail(e.getMessage(), ""); + } + } + + public WechatMerchantTransferResult transferWithAuthorization(String authorizationId, String outAuthorizationNo, + BigDecimal amount, String outBillNo) { + if (StringUtils.isAllBlank(authorizationId, outAuthorizationNo)) { + return WechatMerchantTransferResult.fail("邀请人未开通免确认收款授权", ""); + } + if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) { + return WechatMerchantTransferResult.fail("转账金额必须大于0", ""); + } + try { + Map reqBody = new HashMap<>(); + reqBody.put("appid", config.getAppId()); + reqBody.put("out_bill_no", outBillNo); + reqBody.put("transfer_scene_id", transferSceneId); + reqBody.put("transfer_amount", amount.multiply(BigDecimal.valueOf(100)).intValue()); + reqBody.put("transfer_remark", transferRemark); + reqBody.put("user_recv_perception", userRecvPerception); + if (StringUtils.isNotBlank(authorizationId)) { + reqBody.put("authorization_id", authorizationId); + } else { + reqBody.put("out_authorization_no", outAuthorizationNo); + } + List> reportInfos = buildSceneReportInfos(); + if (!reportInfos.isEmpty()) { + reqBody.put("transfer_scene_report_infos", reportInfos); + } + + String jsonBody = objectMapper.writeValueAsString(reqBody); + Request request = newJsonRequest(transferAuthorizedTransferUrl, jsonBody); + try (Response response = httpClient.newCall(request).execute()) { + String responseBody = response.body() == null ? "" : response.body().string(); + if (!response.isSuccessful()) { + return WechatMerchantTransferResult.fail("微信商家授权转账失败: " + response.code(), responseBody); + } + JsonNode root = objectMapper.readTree(responseBody); + String transferBillNo = root.path("transfer_bill_no").asText(""); + String state = root.path("state").asText(""); + if ("SUCCESS".equals(state)) { + return WechatMerchantTransferResult.success(state, transferBillNo, "", responseBody); + } + return WechatMerchantTransferResult.fail("微信商家授权转账状态: " + state, responseBody); + } + } catch (Exception e) { + return WechatMerchantTransferResult.fail(e.getMessage(), ""); + } + } + + public WechatMerchantTransferResult transferToOpenid(String openid, BigDecimal amount, String outBillNo) { + if (StringUtils.isBlank(openid)) { + return WechatMerchantTransferResult.fail("邀请人缺少小程序openid", ""); + } + if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) { + return WechatMerchantTransferResult.fail("转账金额必须大于0", ""); + } + try { + Map reqBody = new HashMap<>(); + reqBody.put("appid", config.getAppId()); + reqBody.put("out_bill_no", outBillNo); + reqBody.put("transfer_scene_id", transferSceneId); + reqBody.put("openid", openid); + reqBody.put("transfer_amount", amount.multiply(BigDecimal.valueOf(100)).intValue()); + reqBody.put("transfer_remark", transferRemark); + reqBody.put("user_recv_perception", userRecvPerception); + List> reportInfos = buildSceneReportInfos(); + if (!reportInfos.isEmpty()) { + reqBody.put("transfer_scene_report_infos", reportInfos); + } + if (StringUtils.isNotBlank(transferNotifyUrl)) { + reqBody.put("notify_url", transferNotifyUrl); + } + + String jsonBody = objectMapper.writeValueAsString(reqBody); + String authorization = signer.sign("POST", transferUrl, jsonBody); + Request request = new Request.Builder() + .url(transferUrl) + .post(RequestBody.create(MediaType.get("application/json"), jsonBody)) + .addHeader("Authorization", "WECHATPAY2-SHA256-RSA2048 " + authorization) + .addHeader("Content-Type", "application/json") + .addHeader("Accept", "application/json") + .addHeader("User-Agent", "Hiver/1.0") + .build(); + + try (Response response = httpClient.newCall(request).execute()) { + String responseBody = response.body() == null ? "" : response.body().string(); + if (!response.isSuccessful()) { + return WechatMerchantTransferResult.fail("微信商家转账失败: " + response.code(), responseBody); + } + JsonNode root = objectMapper.readTree(responseBody); + String transferBillNo = root.path("transfer_bill_no").asText(""); + String state = root.path("state").asText(""); + String packageInfo = root.path("package_info").asText(""); + if ("SUCCESS".equals(state)) { + return WechatMerchantTransferResult.success(state, transferBillNo, packageInfo, responseBody); + } + if ("WAIT_USER_CONFIRM".equals(state)) { + return WechatMerchantTransferResult.processing(state, transferBillNo, packageInfo, "待邀请人确认收款", responseBody); + } + return WechatMerchantTransferResult.fail("微信商家转账状态: " + state, responseBody); + } + } catch (Exception e) { + return WechatMerchantTransferResult.fail(e.getMessage(), ""); + } + } + + private Request newJsonRequest(String url, String jsonBody) throws Exception { + String authorization = signer.sign("POST", url, jsonBody); + return new Request.Builder() + .url(url) + .post(RequestBody.create(MediaType.get("application/json"), jsonBody)) + .addHeader("Authorization", "WECHATPAY2-SHA256-RSA2048 " + authorization) + .addHeader("Content-Type", "application/json") + .addHeader("Accept", "application/json") + .addHeader("User-Agent", "Hiver/1.0") + .build(); + } + + private List> buildSceneReportInfos() { + List> reportInfos = new ArrayList<>(); + addSceneReportInfo(reportInfos, transferSceneReportInfoType1, transferSceneReportInfoContent1); + addSceneReportInfo(reportInfos, transferSceneReportInfoType2, transferSceneReportInfoContent2); + return reportInfos; + } + + private void addSceneReportInfo(List> reportInfos, String infoType, String infoContent) { + if (StringUtils.isBlank(infoType) || StringUtils.isBlank(infoContent)) { + return; + } + Map info = new HashMap<>(); + info.put("info_type", infoType); + info.put("info_content", infoContent); + reportInfos.add(info); + } +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/invite/service/WechatTransferAuthorizationResult.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/invite/service/WechatTransferAuthorizationResult.java new file mode 100644 index 00000000..df2cf020 --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/invite/service/WechatTransferAuthorizationResult.java @@ -0,0 +1,34 @@ +package cc.hiver.mall.invite.service; + +import lombok.Data; + +@Data +public class WechatTransferAuthorizationResult { + private boolean success; + private String outAuthorizationNo; + private String authorizationId; + private String state; + private String packageInfo; + private String responseBody; + private String message; + + public static WechatTransferAuthorizationResult success(String outAuthorizationNo, String authorizationId, + String state, String packageInfo, String responseBody) { + WechatTransferAuthorizationResult result = new WechatTransferAuthorizationResult(); + result.setSuccess(true); + result.setOutAuthorizationNo(outAuthorizationNo); + result.setAuthorizationId(authorizationId); + result.setState(state); + result.setPackageInfo(packageInfo); + result.setResponseBody(responseBody); + return result; + } + + public static WechatTransferAuthorizationResult fail(String message, String responseBody) { + WechatTransferAuthorizationResult result = new WechatTransferAuthorizationResult(); + result.setSuccess(false); + result.setMessage(message); + result.setResponseBody(responseBody); + return result; + } +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/invite/service/impl/InviteRewardServiceImpl.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/invite/service/impl/InviteRewardServiceImpl.java new file mode 100644 index 00000000..4528b9fa --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/invite/service/impl/InviteRewardServiceImpl.java @@ -0,0 +1,380 @@ +package cc.hiver.mall.invite.service.impl; + +import cc.hiver.core.common.vo.PageVo; +import cc.hiver.core.entity.User; +import cc.hiver.core.service.UserService; +import cc.hiver.mall.entity.ShopArea; +import cc.hiver.mall.invite.entity.InviteRewardConfig; +import cc.hiver.mall.invite.entity.InviteRewardRecord; +import cc.hiver.mall.invite.mapper.InviteRewardConfigMapper; +import cc.hiver.mall.invite.mapper.InviteRewardRecordMapper; +import cc.hiver.mall.invite.service.*; +import cc.hiver.mall.service.ShopAreaService; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import org.apache.commons.lang3.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.Calendar; +import java.util.Date; +import java.util.UUID; + +@Service +public class InviteRewardServiceImpl implements InviteRewardService { + + private static final int STATUS_PROCESSING = 0; + private static final int STATUS_SUCCESS = 1; + private static final int STATUS_FAILED = 2; + private static final int STATUS_SKIPPED = 3; + + @Autowired + private InviteRewardConfigMapper configMapper; + + @Autowired + private InviteRewardRecordMapper recordMapper; + + @Autowired + private UserService userService; + + @Autowired + private ShopAreaService shopAreaService; + + @Autowired + private WechatMerchantTransferService transferService; + + private static String genId() { + return UUID.randomUUID().toString().replace("-", ""); + } + + private Page page(PageVo pageVo) { + int num = pageVo == null || pageVo.getPageNumber() <= 0 ? 1 : pageVo.getPageNumber(); + int size = pageVo == null || pageVo.getPageSize() <= 0 ? 10 : pageVo.getPageSize(); + return new Page<>(num, size); + } + + @Override + public Page pageConfigs(PageVo pageVo, String regionId) { + LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); + if (StringUtils.isNotBlank(regionId)) { + qw.eq(InviteRewardConfig::getRegionId, regionId); + } + qw.orderByDesc(InviteRewardConfig::getCreateTime); + return configMapper.selectPage(page(pageVo), qw); + } + + @Override + public Page pageRecords(PageVo pageVo, String regionId, String inviterId) { + LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); + if (StringUtils.isNotBlank(regionId)) { + qw.eq(InviteRewardRecord::getRegionId, regionId); + } + if (StringUtils.isNotBlank(inviterId)) { + qw.eq(InviteRewardRecord::getInviterId, inviterId); + } + qw.orderByDesc(InviteRewardRecord::getCreateTime); + return recordMapper.selectPage(page(pageVo), qw); + } + + @Override + public InviteRewardConfig saveConfig(InviteRewardConfig config) { + Date now = new Date(); + if (config.getRewardAmount() == null) { + config.setRewardAmount(BigDecimal.ZERO); + } + if (config.getDailyLimit() == null) { + config.setDailyLimit(0); + } + if (config.getTotalBudget() == null) { + config.setTotalBudget(BigDecimal.ZERO); + } + if (config.getUsedBudget() == null) { + config.setUsedBudget(BigDecimal.ZERO); + } + if (config.getEnabled() == null) { + config.setEnabled(1); + } + config.setUpdateTime(now); + if (StringUtils.isBlank(config.getId())) { + config.setId(genId()); + config.setCreateTime(now); + configMapper.insert(config); + } else { + configMapper.updateById(config); + } + return config; + } + + @Override + public void deleteConfig(String id) { + configMapper.deleteById(id); + } + + @Override + public WechatTransferAuthorizationResult applyTransferAuthorization(User user) { + if (user == null || StringUtils.isBlank(user.getId())) { + return WechatTransferAuthorizationResult.fail("用户不存在", ""); + } + if (StringUtils.isBlank(user.getMiniProgramOpenid())) { + return WechatTransferAuthorizationResult.fail("用户缺少小程序openid", ""); + } + String outAuthorizationNo = StringUtils.defaultIfBlank(user.getInviteTransferAuthNo(), buildAuthorizationNo(user)); + WechatTransferAuthorizationResult queryResult = transferService.queryAuthorization(outAuthorizationNo, true); + if (queryResult.isSuccess()) { + saveAuthorizationResult(user, outAuthorizationNo, queryResult); + if ("TAKING_EFFECT".equals(queryResult.getState()) || StringUtils.isNotBlank(queryResult.getPackageInfo())) { + return queryResult; + } + } else if (!isAuthorizationNotFound(queryResult)) { + return queryResult; + } + WechatTransferAuthorizationResult result = transferService.applyAuthorization( + user.getMiniProgramOpenid(), + StringUtils.defaultIfBlank(user.getNickname(), StringUtils.defaultIfBlank(user.getUsername(), user.getMobile())), + outAuthorizationNo + ); + if (result.isSuccess()) { + saveAuthorizationResult(user, outAuthorizationNo, result); + } + return result; + } + + @Override + public WechatTransferAuthorizationResult refreshTransferAuthorization(User user, boolean displayAuthorization) { + if (user == null || StringUtils.isBlank(user.getId())) { + return WechatTransferAuthorizationResult.fail("用户不存在", ""); + } + String outAuthorizationNo = StringUtils.defaultIfBlank(user.getInviteTransferAuthNo(), buildAuthorizationNo(user)); + WechatTransferAuthorizationResult result = transferService.queryAuthorization(outAuthorizationNo, displayAuthorization); + if (result.isSuccess()) { + saveAuthorizationResult(user, outAuthorizationNo, result); + } + return result; + } + + private void saveAuthorizationResult(User user, String outAuthorizationNo, WechatTransferAuthorizationResult result) { + Date now = new Date(); + user.setInviteTransferAuthNo(outAuthorizationNo); + user.setInviteTransferAuthState(result.getState()); + if (StringUtils.isNotBlank(result.getAuthorizationId())) { + user.setInviteTransferAuthId(result.getAuthorizationId()); + } + if ("TAKING_EFFECT".equals(result.getState())) { + user.setInviteTransferAuthTime(now); + } + userService.update(user); + } + + private boolean isAuthorizationNotFound(WechatTransferAuthorizationResult result) { + String message = StringUtils.defaultString(result == null ? "" : result.getMessage()); + String body = StringUtils.defaultString(result == null ? "" : result.getResponseBody()); + return message.contains("404") + || body.contains("RESOURCE_NOT_EXISTS") + || body.contains("NOT_FOUND") + || body.contains("不存在"); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public InviteRewardResult rewardAfterPhoneBound(User invitedUser, String phoneNumber, String inviterId, String regionId, + String deviceId, Double lat, Double lng, boolean firstPhoneBind) { + if (invitedUser == null || StringUtils.isBlank(invitedUser.getId())) { + return InviteRewardResult.skipped("用户不存在"); + } + String finalInviterId = StringUtils.defaultIfBlank(invitedUser.getInviterId(), inviterId); + String finalRegionId = StringUtils.defaultIfBlank(invitedUser.getInviteRegionId(), regionId); + String finalDeviceId = StringUtils.defaultIfBlank(invitedUser.getInviteDeviceId(), deviceId); + if (!firstPhoneBind) { + return InviteRewardResult.skipped("非首次绑定手机号"); + } + if (!Integer.valueOf(1).equals(invitedUser.getInviteNewUser())) { + return InviteRewardResult.skipped("非邀请链路新用户"); + } + if (StringUtils.isBlank(finalInviterId) || StringUtils.isBlank(finalRegionId)) { + return InviteRewardResult.skipped("缺少邀请信息"); + } + if (StringUtils.equals(finalInviterId, invitedUser.getId())) { + markUserSkipped(invitedUser, finalRegionId, finalDeviceId); + return InviteRewardResult.skipped("不能邀请自己"); + } + User inviter = userService.findById(finalInviterId); + if (inviter == null || StringUtils.isBlank(inviter.getMiniProgramOpenid())) { + markUserSkipped(invitedUser, finalRegionId, finalDeviceId); + return InviteRewardResult.skipped("邀请人缺少微信收款身份"); + } + if (!"TAKING_EFFECT".equals(inviter.getInviteTransferAuthState()) + || StringUtils.isAllBlank(inviter.getInviteTransferAuthId(), inviter.getInviteTransferAuthNo())) { + return InviteRewardResult.skipped("邀请人未开通免确认收款授权"); + } + if (!isLocationInRegion(finalRegionId, lat, lng)) { + markUserSkipped(invitedUser, finalRegionId, finalDeviceId); + return InviteRewardResult.skipped("当前位置不在邀请区域内"); + } + InviteRewardConfig config = getEnabledConfig(finalRegionId); + if (config == null || config.getRewardAmount() == null || config.getRewardAmount().compareTo(BigDecimal.ZERO) <= 0) { + return InviteRewardResult.skipped("区域未配置拉新奖励"); + } + if (hasAnyRewardRecord(invitedUser.getId())) { + return InviteRewardResult.skipped("该用户拉新奖励已处理"); + } + if (hasSuccessfulReward(invitedUser.getId(), phoneNumber, finalDeviceId)) { + markUserSkipped(invitedUser, finalRegionId, finalDeviceId); + return InviteRewardResult.skipped("手机号或设备已领取过拉新奖励"); + } + if (exceedDailyLimit(config) || exceedTotalBudget(config)) { + return InviteRewardResult.skipped("区域奖励预算或上限已用完"); + } + + InviteRewardRecord record = createProcessingRecord(invitedUser, phoneNumber, finalInviterId, finalRegionId, finalDeviceId, config.getRewardAmount()); + WechatMerchantTransferResult transferResult = transferService.transferWithAuthorization( + inviter.getInviteTransferAuthId(), + inviter.getInviteTransferAuthNo(), + config.getRewardAmount(), + record.getOutBillNo() + ); + Date now = new Date(); + record.setUpdateTime(now); + record.setWxResponse(transferResult.getResponseBody()); + record.setTransferBillNo(transferResult.getTransferBillNo()); + if (transferResult.isSuccess()) { + record.setStatus(STATUS_SUCCESS); + recordMapper.updateById(record); + config.setUsedBudget((config.getUsedBudget() == null ? BigDecimal.ZERO : config.getUsedBudget()).add(config.getRewardAmount())); + config.setUpdateTime(now); + configMapper.updateById(config); + invitedUser.setInviteRegionId(finalRegionId); + invitedUser.setInviteDeviceId(finalDeviceId); + invitedUser.setInviteRewardStatus(STATUS_SUCCESS); + invitedUser.setInviteRewardAmount(config.getRewardAmount()); + invitedUser.setInviteRewardTime(now); + userService.update(invitedUser); + return InviteRewardResult.rewarded(config.getRewardAmount(), "拉新奖励已发放"); + } + if (transferResult.isProcessing()) { + record.setStatus(STATUS_PROCESSING); + record.setFailReason(StringUtils.defaultIfBlank(transferResult.getMessage(), "待邀请人确认收款")); + recordMapper.updateById(record); + invitedUser.setInviteRegionId(finalRegionId); + invitedUser.setInviteDeviceId(finalDeviceId); + invitedUser.setInviteRewardStatus(STATUS_PROCESSING); + userService.update(invitedUser); + return InviteRewardResult.processing(record.getFailReason()); + } + record.setStatus(STATUS_FAILED); + record.setFailReason(StringUtils.defaultIfBlank(transferResult.getMessage(), "微信商家转账失败")); + recordMapper.updateById(record); + invitedUser.setInviteRegionId(finalRegionId); + invitedUser.setInviteDeviceId(finalDeviceId); + invitedUser.setInviteRewardStatus(STATUS_FAILED); + userService.update(invitedUser); + return InviteRewardResult.failed(record.getFailReason()); + } + + @Override + public InviteRewardConfig getEnabledConfig(String regionId) { + if (StringUtils.isBlank(regionId)) { + return null; + } + return configMapper.selectOne(new LambdaQueryWrapper() + .eq(InviteRewardConfig::getRegionId, regionId) + .eq(InviteRewardConfig::getEnabled, 1) + .last("limit 1")); + } + + private boolean isLocationInRegion(String regionId, Double lat, Double lng) { + if (StringUtils.isBlank(regionId) || lat == null || lng == null) { + return false; + } + ShopArea matched = shopAreaService.matchByLocation(lat, lng); + return matched != null && StringUtils.equals(regionId, matched.getId()); + } + + private boolean hasSuccessfulReward(String invitedUserId, String phoneNumber, String deviceId) { + Long userCount = recordMapper.selectCount(new LambdaQueryWrapper() + .eq(InviteRewardRecord::getStatus, STATUS_SUCCESS) + .eq(InviteRewardRecord::getInvitedUserId, invitedUserId)); + if (userCount != null && userCount > 0) { + return true; + } + if (StringUtils.isNotBlank(phoneNumber)) { + Long mobileCount = recordMapper.selectCount(new LambdaQueryWrapper() + .eq(InviteRewardRecord::getStatus, STATUS_SUCCESS) + .eq(InviteRewardRecord::getInvitedMobile, phoneNumber)); + if (mobileCount != null && mobileCount > 0) { + return true; + } + } + if (StringUtils.isNotBlank(deviceId)) { + Long deviceCount = recordMapper.selectCount(new LambdaQueryWrapper() + .eq(InviteRewardRecord::getStatus, STATUS_SUCCESS) + .eq(InviteRewardRecord::getInviteDeviceId, deviceId)); + return deviceCount != null && deviceCount > 0; + } + return false; + } + + private boolean hasAnyRewardRecord(String invitedUserId) { + Long count = recordMapper.selectCount(new LambdaQueryWrapper() + .eq(InviteRewardRecord::getInvitedUserId, invitedUserId)); + return count != null && count > 0; + } + + private boolean exceedDailyLimit(InviteRewardConfig config) { + if (config.getDailyLimit() == null || config.getDailyLimit() <= 0) { + return false; + } + Calendar calendar = Calendar.getInstance(); + calendar.set(Calendar.HOUR_OF_DAY, 0); + calendar.set(Calendar.MINUTE, 0); + calendar.set(Calendar.SECOND, 0); + calendar.set(Calendar.MILLISECOND, 0); + Long count = recordMapper.selectCount(new LambdaQueryWrapper() + .eq(InviteRewardRecord::getRegionId, config.getRegionId()) + .eq(InviteRewardRecord::getStatus, STATUS_SUCCESS) + .ge(InviteRewardRecord::getCreateTime, calendar.getTime())); + return count != null && count >= config.getDailyLimit(); + } + + private boolean exceedTotalBudget(InviteRewardConfig config) { + BigDecimal totalBudget = config.getTotalBudget() == null ? BigDecimal.ZERO : config.getTotalBudget(); + if (totalBudget.compareTo(BigDecimal.ZERO) <= 0) { + return false; + } + BigDecimal usedBudget = config.getUsedBudget() == null ? BigDecimal.ZERO : config.getUsedBudget(); + BigDecimal rewardAmount = config.getRewardAmount() == null ? BigDecimal.ZERO : config.getRewardAmount(); + return usedBudget.add(rewardAmount).compareTo(totalBudget) > 0; + } + + private InviteRewardRecord createProcessingRecord(User invitedUser, String phoneNumber, String inviterId, String regionId, + String deviceId, BigDecimal amount) { + Date now = new Date(); + InviteRewardRecord record = new InviteRewardRecord(); + record.setId(genId()); + record.setInviterId(inviterId); + record.setInvitedUserId(invitedUser.getId()); + record.setInvitedMobile(phoneNumber); + record.setInviteDeviceId(deviceId); + record.setRegionId(regionId); + record.setAmount(amount); + record.setStatus(STATUS_PROCESSING); + record.setOutBillNo(record.getId()); + record.setCreateTime(now); + record.setUpdateTime(now); + recordMapper.insert(record); + return record; + } + + private String buildAuthorizationNo(User user) { + String userId = user == null ? "" : StringUtils.defaultString(user.getId()); + return ("IA" + userId).replaceAll("[^A-Za-z0-9]", ""); + } + + private void markUserSkipped(User user, String regionId, String deviceId) { + user.setInviteRegionId(regionId); + user.setInviteDeviceId(deviceId); + user.setInviteRewardStatus(STATUS_SKIPPED); + userService.update(user); + } +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/controller/PlanetAdminController.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/controller/PlanetAdminController.java index d932881f..656c70c5 100644 --- a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/controller/PlanetAdminController.java +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/planet/controller/PlanetAdminController.java @@ -4,6 +4,8 @@ import cc.hiver.core.common.exception.HiverException; import cc.hiver.core.common.utils.ResultUtil; import cc.hiver.core.common.vo.PageVo; import cc.hiver.core.common.vo.Result; +import cc.hiver.mall.invite.entity.InviteRewardConfig; +import cc.hiver.mall.invite.service.InviteRewardService; import cc.hiver.mall.planet.entity.*; import cc.hiver.mall.planet.service.PlanetAdminService; import io.swagger.annotations.Api; @@ -32,6 +34,9 @@ public class PlanetAdminController { @Autowired private cc.hiver.mall.planet.service.PlanetAdventureService adventureService; + @Autowired + private InviteRewardService inviteRewardService; + /** 商圈强隔离:白嫖星球后台所有功能均限定在本商圈内 */ private static final String REGION_REQUIRED = "请先选择商圈,白嫖星球仅限本区域内进行"; @@ -119,6 +124,45 @@ public class PlanetAdminController { return ResultUtil.success("删除成功"); } + // ---------------- 拉新返现配置 ---------------- + @ApiOperation(value = "拉新返现配置分页") + @RequestMapping(value = "/inviteReward/page", method = RequestMethod.GET) + public Result inviteRewardPage(@RequestParam(required = false) Integer pageNumber, + @RequestParam(required = false) Integer pageSize, + @RequestParam(required = false) String regionId) { + if (StringUtils.isEmpty(regionId)) { + return ResultUtil.error(REGION_REQUIRED); + } + return new ResultUtil<>().setData(inviteRewardService.pageConfigs(buildPage(pageNumber, pageSize), regionId)); + } + + @ApiOperation(value = "保存拉新返现配置") + @RequestMapping(value = "/inviteReward/save", method = RequestMethod.POST) + public Result inviteRewardSave(@RequestBody InviteRewardConfig config) { + if (config == null || StringUtils.isEmpty(config.getRegionId())) { + return ResultUtil.error("拉新返现配置必须归属商圈"); + } + return new ResultUtil<>().setData(inviteRewardService.saveConfig(config)); + } + + @ApiOperation(value = "删除拉新返现配置") + @RequestMapping(value = "/inviteReward/delete", method = RequestMethod.POST) + public Result inviteRewardDelete(@RequestParam String id) { + inviteRewardService.deleteConfig(id); + return ResultUtil.success("删除成功"); + } + + @ApiOperation(value = "拉新返现记录分页") + @RequestMapping(value = "/inviteReward/record/page", method = RequestMethod.GET) + public Result inviteRewardRecordPage(@RequestParam(required = false) Integer pageNumber, + @RequestParam(required = false) Integer pageSize, + @RequestParam(required = false) String regionId) { + if (StringUtils.isEmpty(regionId)) { + return ResultUtil.error(REGION_REQUIRED); + } + return new ResultUtil<>().setData(inviteRewardService.pageRecords(buildPage(pageNumber, pageSize), regionId, null)); + } + // ---------------- BUFF ---------------- @ApiOperation(value = "BUFF分页") @RequestMapping(value = "/buff/page", method = RequestMethod.GET) diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/utils/WechatPaySigner.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/utils/WechatPaySigner.java index bc8c71c6..14701cfa 100644 --- a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/utils/WechatPaySigner.java +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/utils/WechatPaySigner.java @@ -68,7 +68,8 @@ public class WechatPaySigner { } String path = httpUrl.encodedPath(); // 如 "/v3/pay/transactions/jsapi" - String query = httpUrl.query(); // 如 "a=1&b=2",若无则为 null + String query = httpUrl.encodedQuery(); // 如 "a=1&b=2",若无则为 null + String urlPath = path + (query == null ? "" : "?" + query); String nonceStr = UUID.randomUUID().toString().replace("-", "").substring(0, 32); long timestamp = System.currentTimeMillis() / 1000; // 秒! @@ -76,7 +77,7 @@ public class WechatPaySigner { // 构建签名消息(严格按格式) StringBuilder sb = new StringBuilder(); sb.append(method).append("\n") - .append(path).append("\n") + .append(urlPath).append("\n") .append(timestamp).append("\n") .append(nonceStr).append("\n") .append(body == null ? "" : body).append("\n"); diff --git a/hiver-modules/hiver-mall/src/main/resources/db/invite_reward.sql b/hiver-modules/hiver-mall/src/main/resources/db/invite_reward.sql new file mode 100644 index 00000000..fc83d925 --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/resources/db/invite_reward.sql @@ -0,0 +1,48 @@ +CREATE TABLE IF NOT EXISTS `t_invite_reward_config` ( + `id` varchar(64) NOT NULL COMMENT '主键', + `region_id` varchar(64) NOT NULL COMMENT '区域id', + `reward_amount` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '每个有效新用户奖励金额', + `daily_limit` int(11) NOT NULL DEFAULT '0' COMMENT '每日发放上限 0不限', + `total_budget` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '总预算 0不限', + `used_budget` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '已发放预算', + `enabled` int(11) NOT NULL DEFAULT '1' COMMENT '是否启用 1是 0否', + `remark` varchar(255) DEFAULT NULL COMMENT '备注', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `update_time` datetime DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_invite_reward_config_region` (`region_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='拉新返现配置'; + +CREATE TABLE IF NOT EXISTS `t_invite_reward_record` ( + `id` varchar(64) NOT NULL COMMENT '主键', + `inviter_id` varchar(64) NOT NULL COMMENT '邀请人用户id', + `invited_user_id` varchar(64) NOT NULL COMMENT '被邀请人用户id', + `invited_mobile` varchar(32) DEFAULT NULL COMMENT '被邀请人手机号', + `invite_device_id` varchar(128) DEFAULT NULL COMMENT '邀请设备标识', + `region_id` varchar(64) NOT NULL COMMENT '发生奖励时的区域id', + `amount` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '奖励金额', + `status` int(11) NOT NULL DEFAULT '0' COMMENT '状态 0处理中 1成功 2失败 3跳过', + `out_bill_no` varchar(64) DEFAULT NULL COMMENT '微信商家转账商户单号', + `transfer_bill_no` varchar(128) DEFAULT NULL COMMENT '微信商家转账单号', + `fail_reason` varchar(512) DEFAULT NULL COMMENT '失败或跳过原因', + `wx_response` text COMMENT '微信响应', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `update_time` datetime DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_invite_reward_invited_user` (`invited_user_id`), + KEY `idx_invite_reward_mobile` (`invited_mobile`), + KEY `idx_invite_reward_device` (`invite_device_id`), + KEY `idx_invite_reward_region_status` (`region_id`, `status`, `create_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='拉新返现奖励记录'; + +ALTER TABLE `t_user` + ADD COLUMN `invite_region_id` varchar(64) DEFAULT NULL COMMENT '拉新来源区域id', + ADD COLUMN `invite_device_id` varchar(128) DEFAULT NULL COMMENT '拉新设备标识', + ADD COLUMN `invite_new_user` int(11) DEFAULT '0' COMMENT '是否邀请链路新用户 1是 0否', + ADD COLUMN `invite_reward_status` int(11) DEFAULT '0' COMMENT '拉新奖励状态 0未处理 1成功 2失败 3跳过', + ADD COLUMN `invite_reward_amount` decimal(10,2) DEFAULT '0.00' COMMENT '拉新奖励金额', + ADD COLUMN `invite_reward_time` datetime DEFAULT NULL COMMENT '拉新奖励时间', + ADD COLUMN `invite_transfer_auth_no` varchar(64) DEFAULT NULL COMMENT '拉新转账免确认授权商户单号', + ADD COLUMN `invite_transfer_auth_id` varchar(64) DEFAULT NULL COMMENT '拉新转账微信免确认授权单号', + ADD COLUMN `invite_transfer_auth_state` varchar(32) DEFAULT NULL COMMENT '拉新转账免确认授权状态', + ADD COLUMN `invite_transfer_auth_time` datetime DEFAULT NULL COMMENT '拉新转账免确认授权时间'; diff --git a/hiver-modules/hiver-social/src/main/java/cc/hiver/social/controller/WechatController.java b/hiver-modules/hiver-social/src/main/java/cc/hiver/social/controller/WechatController.java index e89a4b73..9faf180c 100644 --- a/hiver-modules/hiver-social/src/main/java/cc/hiver/social/controller/WechatController.java +++ b/hiver-modules/hiver-social/src/main/java/cc/hiver/social/controller/WechatController.java @@ -11,6 +11,8 @@ import cc.hiver.core.common.vo.Result; import cc.hiver.core.config.properties.HiverTokenProperties; import cc.hiver.core.entity.User; import cc.hiver.core.service.UserService; +import cc.hiver.mall.invite.service.InviteRewardResult; +import cc.hiver.mall.invite.service.InviteRewardService; import cc.hiver.mall.service.mybatis.CustomerService; import cc.hiver.social.entity.Social; import cc.hiver.social.service.SocialService; @@ -109,6 +111,9 @@ public class WechatController { @Autowired private WechatPhoneService wechatPhoneService; + @Autowired + private InviteRewardService inviteRewardService; + @Autowired private RedisTemplateHelper redisTemplateHelper; @@ -144,15 +149,37 @@ public class WechatController { @PostMapping("/getPhone") @ResponseBody - public Result getUserPhone(@RequestParam(required = false) String code) { + public Result getUserPhone(@RequestParam(required = false) String code, + @RequestParam(required = false) String inviterId, + @RequestParam(required = false) String regionId, + @RequestParam(required = false) String deviceId, + @RequestParam(required = false) Double lat, + @RequestParam(required = false) Double lng) { String phoneNumber = wechatPhoneService.getPhoneNumber(code); if (CharSequenceUtil.isNotBlank(phoneNumber)) { try { final User u = securityUtil.getCurrUser(); String key = "username::" + u.getUsername(); + boolean firstPhoneBind = CharSequenceUtil.isBlank(u.getMobile()); + User phoneOwner = userService.findByMobile(phoneNumber); + if (phoneOwner != null && CharSequenceUtil.isNotBlank(phoneOwner.getId()) + && !phoneOwner.getId().equals(u.getId())) { + return ResultUtil.error("该手机号已绑定其他账号"); + } u.setMobile(phoneNumber); + if (CharSequenceUtil.isNotBlank(inviterId) && CharSequenceUtil.isBlank(u.getInviterId())) { + u.setInviterId(inviterId); + } + if (CharSequenceUtil.isNotBlank(regionId) && CharSequenceUtil.isBlank(u.getInviteRegionId())) { + u.setInviteRegionId(regionId); + } + if (CharSequenceUtil.isNotBlank(deviceId) && CharSequenceUtil.isBlank(u.getInviteDeviceId())) { + u.setInviteDeviceId(deviceId); + } userService.update(u); + InviteRewardResult rewardResult = inviteRewardService.rewardAfterPhoneBound( + u, phoneNumber, inviterId, regionId, deviceId, lat, lng, firstPhoneBind); // 删除缓存 redisTemplate.delete(key); //User newUser = userService.findByMobile(phoneNumber); @@ -161,9 +188,13 @@ public class WechatController { final Gson gson = new GsonBuilder().setDateFormat("yyyy-MM-dd hh:mm:ss").create(); redisTemplateHelper.set(keyNew, gson.toJson(u), tokenProperties.getSaveLoginTime(), TimeUnit.DAYS); final String accessToken = securityUtil.getToken(userService.userToDTO(u), true); - Map map = new HashMap<>(); + Map map = new HashMap<>(); map.put("mobile",phoneNumber); map.put("accessToken",accessToken); + map.put("inviteRewarded", rewardResult != null && rewardResult.isRewarded()); + map.put("inviteRewardProcessing", rewardResult != null && rewardResult.isProcessing()); + map.put("inviteRewardAmount", rewardResult == null ? null : rewardResult.getAmount()); + map.put("inviteRewardMessage", rewardResult == null ? "" : rewardResult.getMessage()); return ResultUtil.data(map); } catch (Exception e) { log.error("绑定微信手机号失败: phoneNumber={}", phoneNumber, e);