diff --git a/jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/system/vo/AppletUser.java b/jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/system/vo/AppletUser.java index adce048..f8e0527 100644 --- a/jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/system/vo/AppletUser.java +++ b/jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/system/vo/AppletUser.java @@ -108,4 +108,10 @@ public class AppletUser implements Serializable { @Excel(name = "邀请人", width = 15) @Schema(description = "邀请人") private String inviter; + + @Schema(description = "推广官") + private String isPromote; + + @Schema(description = "开通时间") + private String promoteTime; } diff --git a/jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/applet/controller/AppletStatisticsController.java b/jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/applet/controller/AppletStatisticsController.java new file mode 100644 index 0000000..b3ca98f --- /dev/null +++ b/jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/applet/controller/AppletStatisticsController.java @@ -0,0 +1,119 @@ +package org.jeecg.modules.applet.controller; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.jeecg.common.api.IAppletUserService; +import org.jeecg.common.api.vo.Result; +import org.jeecg.common.system.vo.AppletUser; +import org.jeecg.config.shiro.IgnoreAuth; +import org.jeecg.modules.applet.entity.StatisticsItem; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +/** + * @Description: 管理系统统计数据接口 + * @Author: jeecg-boot + * @Date: 2025-01-27 + * @Version: V1.0 + */ +@Slf4j +@Tag(name = "管理系统统计", description = "管理系统统计数据接口") +@RestController +@RequestMapping("/statistics") +public class AppletStatisticsController { + + @Autowired + private IAppletUserService appletUserService; + + /** + * 获取管理系统统计数据 + * 包含:总用户数、今日新增注册用户数、本月新增注册用户数、总推广官数 + * + * @return 统计数据列表 + */ + @Operation(summary = "获取管理系统统计数据", description = "获取总用户数、今日新增、本月新增、总推广官数等统计数据") + @GetMapping(value = "/dashboard") + @IgnoreAuth + public List getDashboardStatistics() { + try { + List statisticsList = new ArrayList<>(); + + // 1. 总用户数 + long totalUsers = appletUserService.count(); + statisticsList.add(StatisticsItem.builder() + .title("总用户数") + .icon("ant-design:user-outlined") + .value(totalUsers) + .color("blue") + .suffix("人") + .build()); + + // 2. 今日新增注册用户数 + LocalDate today = LocalDate.now(); + LocalDateTime todayStart = today.atStartOfDay(); + LocalDateTime todayEnd = today.plusDays(1).atStartOfDay(); + + QueryWrapper todayQuery = new QueryWrapper<>(); + todayQuery.ge("create_time", todayStart); + todayQuery.lt("create_time", todayEnd); + long todayNewUsers = appletUserService.count(todayQuery); + + statisticsList.add(StatisticsItem.builder() + .title("今日新增用户") + .icon("ant-design:user-add-outlined") + .value(todayNewUsers) + .color("green") + .suffix("人") + .build()); + + // 3. 本月新增注册用户数 + LocalDate monthStart = today.withDayOfMonth(1); + LocalDateTime monthStartTime = monthStart.atStartOfDay(); + LocalDateTime monthEndTime = monthStart.plusMonths(1).atStartOfDay(); + + QueryWrapper monthQuery = new QueryWrapper<>(); + monthQuery.ge("create_time", monthStartTime); + monthQuery.lt("create_time", monthEndTime); + long monthNewUsers = appletUserService.count(monthQuery); + + statisticsList.add(StatisticsItem.builder() + .title("本月新增用户") + .icon("ant-design:calendar-outlined") + .value(monthNewUsers) + .color("orange") + .suffix("人") + .build()); + + // 4. 总推广官数(有邀请过其他用户的用户) + long totalPromoters = appletUserService.count(Wrappers.lambdaQuery() + .eq(AppletUser::getIsPromote, "Y")); + + statisticsList.add(StatisticsItem.builder() + .title("总推广官数") + .icon("ant-design:team-outlined") + .value(totalPromoters) + .color("purple") + .suffix("人") + .build()); + + log.info("获取管理系统统计数据成功,总用户数: {}, 今日新增: {}, 本月新增: {}, 总推广官: {}", + totalUsers, todayNewUsers, monthNewUsers, totalPromoters); + + return statisticsList; + + } catch (Exception e) { + log.error("获取管理系统统计数据异常", e); + return null; + } + } +} \ No newline at end of file diff --git a/jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/applet/entity/StatisticsItem.java b/jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/applet/entity/StatisticsItem.java new file mode 100644 index 0000000..bc25c98 --- /dev/null +++ b/jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/applet/entity/StatisticsItem.java @@ -0,0 +1,51 @@ +package org.jeecg.modules.applet.entity; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +/** + * @Description: 统计数据项 + * @Author: jeecg-boot + * @Date: 2025-01-20 + * @Version: V1.0 + */ +@Data +@Accessors(chain = true) +@Schema(description = "统计数据项") +@Builder +public class StatisticsItem { + + /**标题*/ + @Schema(description = "标题") + private String title; + + /**图标*/ + @Schema(description = "图标") + private String icon; + + /**数值*/ + @Schema(description = "数值") + private Object value; + + /**颜色*/ + @Schema(description = "颜色") + private String color; + + /**后缀*/ + @Schema(description = "后缀") + private String suffix; + + public StatisticsItem() {} + + public StatisticsItem(String title, String icon, Object value, String color, String suffix) { + this.title = title; + this.icon = icon; + this.value = value; + this.color = color; + this.suffix = suffix; + } +} \ No newline at end of file diff --git a/jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/applet/service/AppletApiWaterService.java b/jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/applet/service/AppletApiWaterService.java index 00d5ce7..4c50edf 100644 --- a/jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/applet/service/AppletApiWaterService.java +++ b/jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/applet/service/AppletApiWaterService.java @@ -38,4 +38,5 @@ public interface AppletApiWaterService { AppletWithdrawal getWithdraw(AppletWithdrawal appletWithdrawal); StatisticsVo statistics(); + } diff --git a/jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/applet/service/impl/AppletApiWaterServiceImpl.java b/jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/applet/service/impl/AppletApiWaterServiceImpl.java index 2350095..1e14d7d 100644 --- a/jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/applet/service/impl/AppletApiWaterServiceImpl.java +++ b/jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/applet/service/impl/AppletApiWaterServiceImpl.java @@ -2,10 +2,12 @@ package org.jeecg.modules.applet.service.impl; import com.alibaba.fastjson.JSON; import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.toolkit.IdWorker; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import lombok.extern.log4j.Log4j2; import org.apache.commons.lang3.StringUtils; import org.jeecg.common.api.IAppletUserService; +import org.jeecg.common.api.vo.Result; import org.jeecg.common.exception.JeecgBootException; import org.jeecg.common.system.util.AppletUserUtil; import org.jeecg.common.system.vo.AppletUser; @@ -13,12 +15,15 @@ import org.jeecg.modules.applet.entity.StatisticsVo; import org.jeecg.modules.applet.service.AppletApiWaterService; import org.jeecg.modules.common.IdUtils; import org.jeecg.modules.common.wxUtils.WxHttpUtils; +import org.jeecg.modules.common.wxUtils.transfer.TransferToUser; +import org.jeecg.modules.common.wxUtils.transfer.WXPayUtility; import org.jeecg.modules.demo.appletConfig.service.IAppletConfigService; import org.jeecg.modules.demo.appletWater.entity.AppletWater; import org.jeecg.modules.demo.appletWater.service.IAppletWaterService; import org.jeecg.modules.demo.appletWithdrawal.entity.AppletWithdrawal; import org.jeecg.modules.demo.appletWithdrawal.service.IAppletWithdrawalService; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpEntity; import org.springframework.http.HttpMethod; import org.springframework.http.ResponseEntity; @@ -35,6 +40,7 @@ import java.io.File; import java.math.BigDecimal; import java.net.URL; import java.nio.file.Files; +import java.util.ArrayList; import java.util.HashMap; import java.util.Map; @@ -53,6 +59,7 @@ public class AppletApiWaterServiceImpl implements AppletApiWaterService { @Autowired private WxHttpUtils wxHttpUtils; + @Override public byte[] getInviteCode(){ AppletUser user = AppletUserUtil.getCurrentAppletUser(); @@ -249,4 +256,6 @@ public class AppletApiWaterServiceImpl implements AppletApiWaterService { .num(count) .build(); } + + } diff --git a/jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/applet/service/impl/WxAppletService.java b/jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/applet/service/impl/WxAppletService.java index 4fd038c..d46f94c 100644 --- a/jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/applet/service/impl/WxAppletService.java +++ b/jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/applet/service/impl/WxAppletService.java @@ -3,11 +3,20 @@ package org.jeecg.modules.applet.service.impl; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import lombok.extern.slf4j.Slf4j; +import org.jeecg.common.api.IAppletUserService; import org.jeecg.common.api.vo.Result; +import org.jeecg.common.exception.JeecgBootException; +import org.jeecg.common.system.vo.AppletUser; import org.jeecg.modules.common.wxUtils.WxHttpUtils; +import org.jeecg.modules.common.wxUtils.transfer.TransferToUser; +import org.jeecg.modules.common.wxUtils.transfer.WXPayUtility; +import org.jeecg.modules.demo.appletWithdrawal.entity.AppletWithdrawal; 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.Map; @@ -24,6 +33,31 @@ public class WxAppletService { @Autowired private WxHttpUtils wxHttpUtils; + @Autowired + private IAppletUserService appletUserService; + + //微信小程序的 AppID + @Value("${wechat.mpAppId}") + private String appid; + //商户号 + @Value("${wechat.merchantId}") + private String mchid; + //商户API私钥路径 + @Value("${wechat.privateKeyPath}") + private String privateKeyFilePath; + //商户API公钥路径 + @Value("${wechat.publicKeyPath}") + private String wechatPayPublicKeyFilePath; + //商户API公钥ID + @Value("${wechat.publicKeyId}") + private String wechatPayPublicKeyId; + //商户证书序列号 + @Value("${wechat.merchantSerialNumber}") + private String certiticateSerialNo; + //提现转账回调地址 + @Value("${wechat.transferNotify}") + private String transferNotify; + /** * 获取小程序码 * @@ -169,4 +203,101 @@ public class WxAppletService { return Result.error("获取失败: " + e.getMessage()); } } + + + public void approveWithdrawal(AppletWithdrawal withdrawal) { + //权限验证 + AppletUser user = appletUserService.getById(withdrawal.getUserId()); + + //提现结果 + String massage = "提现申请失败"; + + //1.微信提现基础参数 + //变化的用户信息参数 +// map.put("openid", member.getAppletOpenid());//用户openid(小程序) +// map.put("userName", commonMoneyLog.getUserName());//用户真实姓名 +// map.put("transferAmount", commonMoneyLog.getMoney());//提现金额, 单位为“分” +// String idStr = "H" + IdWorker.getIdStr(); +// map.put("outBillNo", idStr);//商户单号 + + TransferToUser client = new TransferToUser( + mchid, // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756 + certiticateSerialNo, // 商户API证书序列号,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053 + privateKeyFilePath, // 商户API证书私钥文件路径,本地文件路径 + wechatPayPublicKeyId, // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816 + wechatPayPublicKeyFilePath // 微信支付公钥文件路径,本地文件路径 + ); + + //2、场景信息 + TransferToUser.TransferToUserRequest request = new TransferToUser.TransferToUserRequest(); + request.appid = appid; + request.outBillNo = withdrawal.getId(); + request.transferSceneId = "1010";//商户转账场景ID 1010-二手回收 + request.openid = user.getOpenid(); + request.userName = withdrawal.getName(); + request.transferAmount = withdrawal.getMoney().multiply(new BigDecimal(100)).longValue();//单位为分 + request.transferRemark = "备注"; + request.notifyUrl = transferNotify; +// request.notifyUrl = "https://www.yurangongfang.com/massage-admin/massage/cash/cashoutNotify/"; + request.userRecvPerception = ""; + request.transferSceneReportInfos = new ArrayList<>(); + { + TransferToUser.TransferSceneReportInfo item0 = new TransferToUser.TransferSceneReportInfo(); + item0.infoType = "回收商品名称"; + item0.infoContent = "衣服"; + request.transferSceneReportInfos.add(item0); + } + + //3、执行提现 + TransferToUser.TransferToUserResponse response = null; + try { + response = client.run(request); + }catch (WXPayUtility.ApiException e){ + if (e.statusCode == 400){ + throw new JeecgBootException("请输入您的真实姓名"); + } + throw new JeecgBootException("提现失败"); + } + + log.info("提现发起成功,outBillNo:"+response.outBillNo + ",transferBillNo:" +response.transferBillNo + ",state:" +response.state+ ",packageInfo:" + response.packageInfo); + switch (response.state){ + case ACCEPTED: + log.info("转账已受理"); + massage = "转账已受理"; + break; + case PROCESSING: + log.info("转账锁定资金中。如果一直停留在该状态,建议检查账户余额是否足够,如余额不足,可充值后再原单重试"); + massage = "转账锁定资金中。如果一直停留在该状态,建议检查账户余额是否足够,如余额不足,可充值后再原单重试"; + break; + case WAIT_USER_CONFIRM: + log.info("待收款用户确认,可拉起微信收款确认页面进行收款确认"); + massage = "待收款用户确认,可拉起微信收款确认页面进行收款确认"; + break; + case TRANSFERING: + log.info("转账中,可拉起微信收款确认页面再次重试确认收款"); + massage = "转账中,可拉起微信收款确认页面再次重试确认收款"; + break; + case SUCCESS: + log.info("转账成功"); + massage = "转账成功"; + break; + case FAIL: + log.info("转账失败"); + massage = "转账失败"; + break; + case CANCELING: + log.info("商户撤销请求受理成功,该笔转账正在撤销中"); + massage = "商户撤销请求受理成功,该笔转账正在撤销中"; + break; + case CANCELLED: + log.info("转账撤销完成"); + massage = "转账撤销完成"; + break; + } + + log.info("提现结果:" + massage); + + withdrawal.setPackageInfo(response.packageInfo);//参数 + + } } \ No newline at end of file diff --git a/jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/common/wxUtils/transfer/TransferToUser.java b/jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/common/wxUtils/transfer/TransferToUser.java new file mode 100644 index 0000000..7bf7bf0 --- /dev/null +++ b/jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/common/wxUtils/transfer/TransferToUser.java @@ -0,0 +1,153 @@ +package org.jeecg.modules.common.wxUtils.transfer; + +import com.google.gson.annotations.SerializedName; +import lombok.extern.slf4j.Slf4j; +import okhttp3.*; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.util.List; +import java.util.Map; + +/** + * 发起转账 + */ +@Slf4j +public class TransferToUser { + + private final String mchid; + private final String certificateSerialNo; + private final PrivateKey privateKey; + private final String wechatPayPublicKeyId; + private final PublicKey wechatPayPublicKey; + + public TransferToUser(String mchid, String certificateSerialNo, String privateKeyFilePath, String wechatPayPublicKeyId, String wechatPayPublicKeyFilePath) { + this.mchid = mchid; + this.certificateSerialNo = certificateSerialNo; + this.privateKey = WXPayUtility.loadPrivateKeyFromPath(privateKeyFilePath); + this.wechatPayPublicKeyId = wechatPayPublicKeyId; + this.wechatPayPublicKey = WXPayUtility.loadPublicKeyFromPath(wechatPayPublicKeyFilePath); + } + + public TransferToUserResponse run(TransferToUserRequest request) { + +// map.put("host", "https://api.mch.weixin.qq.com");//请求地址 +// map.put("method", "POST");//请求类型 +// map.put("path", "/v3/fund-app/mch-transfer/transfer-bills");//提现接口 + + String uri = "https://api.mch.weixin.qq.com"; + String host = "https://api.mch.weixin.qq.com"; + String method = "POST"; + String reqBody = WXPayUtility.toJson(request); + + Request.Builder reqBuilder = new Request.Builder().url(host + uri); + reqBuilder.addHeader("Accept", "application/json"); + reqBuilder.addHeader("Wechatpay-Serial", wechatPayPublicKeyId); + reqBuilder.addHeader("Authorization", WXPayUtility.buildAuthorization(mchid, certificateSerialNo,privateKey, method, uri, reqBody)); + reqBuilder.addHeader("Content-Type", "application/json"); + RequestBody requestBody = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), reqBody); + reqBuilder.method(method, requestBody); + Request httpRequest = reqBuilder.build(); + + // 发送HTTP请求 + OkHttpClient client = new OkHttpClient.Builder().build(); + try (Response httpResponse = client.newCall(httpRequest).execute()) { + String respBody = WXPayUtility.extractBody(httpResponse); + if (httpResponse.code() >= 200 && httpResponse.code() < 300) { + // 2XX 成功,验证应答签名 + WXPayUtility.validateResponse(this.wechatPayPublicKeyId, this.wechatPayPublicKey, + httpResponse.headers(), respBody); + + // 从HTTP应答报文构建返回数据 + return WXPayUtility.fromJson(respBody, TransferToUserResponse.class); + } else { + throw new WXPayUtility.ApiException(httpResponse.code(), respBody, httpResponse.headers()); + } + } catch (IOException e) { + throw new UncheckedIOException("Sending request to " + uri + " failed.", e); + } + } + + public String encrypt(String plainText) { + return WXPayUtility.encrypt(this.wechatPayPublicKey, plainText); + } + + public static class TransferToUserResponse { + @SerializedName("out_bill_no") + public String outBillNo; + + @SerializedName("transfer_bill_no") + public String transferBillNo; + + @SerializedName("create_time") + public String createTime; + + @SerializedName("state") + public TransferBillStatus state; + + @SerializedName("package_info") + public String packageInfo; + } + + public enum TransferBillStatus { + @SerializedName("ACCEPTED") + ACCEPTED, + @SerializedName("PROCESSING") + PROCESSING, + @SerializedName("WAIT_USER_CONFIRM") + WAIT_USER_CONFIRM, + @SerializedName("TRANSFERING") + TRANSFERING, + @SerializedName("SUCCESS") + SUCCESS, + @SerializedName("FAIL") + FAIL, + @SerializedName("CANCELING") + CANCELING, + @SerializedName("CANCELLED") + CANCELLED + } + + public static class TransferSceneReportInfo { + @SerializedName("info_type") + public String infoType; + + @SerializedName("info_content") + public String infoContent; + } + + public static class TransferToUserRequest { + @SerializedName("appid") + public String appid; + + @SerializedName("out_bill_no") + public String outBillNo; + + @SerializedName("transfer_scene_id") + public String transferSceneId; + + @SerializedName("openid") + public String openid; + + @SerializedName("user_name") + public String userName; + + @SerializedName("transfer_amount") + public Long transferAmount; + + @SerializedName("transfer_remark") + public String transferRemark; + + @SerializedName("notify_url") + public String notifyUrl; + + @SerializedName("user_recv_perception") + public String userRecvPerception; + + @SerializedName("transfer_scene_report_infos") + public List transferSceneReportInfos; + } + +} diff --git a/jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/common/wxUtils/transfer/WXPayUtility.java b/jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/common/wxUtils/transfer/WXPayUtility.java new file mode 100644 index 0000000..36882d3 --- /dev/null +++ b/jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/common/wxUtils/transfer/WXPayUtility.java @@ -0,0 +1,381 @@ +package org.jeecg.modules.common.wxUtils.transfer; + +import com.google.gson.*; +import com.google.gson.annotations.Expose; +import okhttp3.Headers; +import okhttp3.Response; +import okio.BufferedSource; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.security.*; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; +import java.time.DateTimeException; +import java.time.Duration; +import java.time.Instant; +import java.util.Base64; +import java.util.Map; +import java.util.Objects; + +public class WXPayUtility { + private static final Gson gson = new GsonBuilder() + .disableHtmlEscaping() + .addSerializationExclusionStrategy(new ExclusionStrategy() { + @Override + public boolean shouldSkipField(FieldAttributes fieldAttributes) { + final Expose expose = fieldAttributes.getAnnotation(Expose.class); + return expose != null && !expose.serialize(); + } + + @Override + public boolean shouldSkipClass(Class aClass) { + return false; + } + }) + .addDeserializationExclusionStrategy(new ExclusionStrategy() { + @Override + public boolean shouldSkipField(FieldAttributes fieldAttributes) { + final Expose expose = fieldAttributes.getAnnotation(Expose.class); + return expose != null && !expose.deserialize(); + } + + @Override + public boolean shouldSkipClass(Class aClass) { + return false; + } + }) + .create(); + private static final char[] SYMBOLS = + "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray(); + private static final SecureRandom random = new SecureRandom(); + + /** + * 将 Object 转换为 JSON 字符串 + */ + public static String toJson(Object object) { + return gson.toJson(object); + } + + /** + * 将 JSON 字符串解析为特定类型的实例 + */ + public static T fromJson(String json, Class classOfT) throws JsonSyntaxException { + return gson.fromJson(json, classOfT); + } + + /** + * 从公私钥文件路径中读取文件内容 + * + * @param keyPath 文件路径 + * @return 文件内容 + */ + private static String readKeyStringFromPath(String keyPath) { + try { + return new String(Files.readAllBytes(Paths.get(keyPath)), StandardCharsets.UTF_8); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + /** + * 读取 PKCS#8 格式的私钥字符串并加载为私钥对象 + * + * @param keyString 私钥文件内容,以 -----BEGIN PRIVATE KEY----- 开头 + * @return PrivateKey 对象 + */ + public static PrivateKey loadPrivateKeyFromString(String keyString) { + try { + keyString = keyString.replace("-----BEGIN PRIVATE KEY-----", "") + .replace("-----END PRIVATE KEY-----", "") + .replaceAll("\\s+", ""); + return KeyFactory.getInstance("RSA").generatePrivate( + new PKCS8EncodedKeySpec(Base64.getDecoder().decode(keyString))); + } catch (NoSuchAlgorithmException e) { + throw new UnsupportedOperationException(e); + } catch (InvalidKeySpecException e) { + throw new IllegalArgumentException(e); + } + } + + /** + * 从 PKCS#8 格式的私钥文件中加载私钥 + * + * @param keyPath 私钥文件路径 + * @return PrivateKey 对象 + */ + public static PrivateKey loadPrivateKeyFromPath(String keyPath) { + return loadPrivateKeyFromString(readKeyStringFromPath(keyPath)); + } + + /** + * 读取 PKCS#8 格式的公钥字符串并加载为公钥对象 + * + * @param keyString 公钥文件内容,以 -----BEGIN PUBLIC KEY----- 开头 + * @return PublicKey 对象 + */ + public static PublicKey loadPublicKeyFromString(String keyString) { + try { + keyString = keyString.replace("-----BEGIN PUBLIC KEY-----", "") + .replace("-----END PUBLIC KEY-----", "") + .replaceAll("\\s+", ""); + return KeyFactory.getInstance("RSA").generatePublic( + new X509EncodedKeySpec(Base64.getDecoder().decode(keyString))); + } catch (NoSuchAlgorithmException e) { + throw new UnsupportedOperationException(e); + } catch (InvalidKeySpecException e) { + throw new IllegalArgumentException(e); + } + } + + /** + * 从 PKCS#8 格式的公钥文件中加载公钥 + * + * @param keyPath 公钥文件路径 + * @return PublicKey 对象 + */ + public static PublicKey loadPublicKeyFromPath(String keyPath) { + return loadPublicKeyFromString(readKeyStringFromPath(keyPath)); + } + + /** + * 创建指定长度的随机字符串,字符集为[0-9a-zA-Z],可用于安全相关用途 + */ + public static String createNonce(int length) { + char[] buf = new char[length]; + for (int i = 0; i < length; ++i) { + buf[i] = SYMBOLS[random.nextInt(SYMBOLS.length)]; + } + return new String(buf); + } + + /** + * 使用公钥按照 RSA_PKCS1_OAEP_PADDING 算法进行加密 + * + * @param publicKey 加密用公钥对象 + * @param plaintext 待加密明文 + * @return 加密后密文 + */ + public static String encrypt(PublicKey publicKey, String plaintext) { + final String transformation = "RSA/ECB/OAEPWithSHA-1AndMGF1Padding"; + + try { + Cipher cipher = Cipher.getInstance(transformation); + cipher.init(Cipher.ENCRYPT_MODE, publicKey); + return Base64.getEncoder().encodeToString(cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8))); + } catch (NoSuchAlgorithmException | NoSuchPaddingException e) { + throw new IllegalArgumentException("The current Java environment does not support " + transformation, e); + } catch (InvalidKeyException e) { + throw new IllegalArgumentException("RSA encryption using an illegal publicKey", e); + } catch (BadPaddingException | IllegalBlockSizeException e) { + throw new IllegalArgumentException("Plaintext is too long", e); + } + } + + /** + * 使用私钥按照指定算法进行签名 + * + * @param message 待签名串 + * @param algorithm 签名算法,如 SHA256withRSA + * @param privateKey 签名用私钥对象 + * @return 签名结果 + */ + public static String sign(String message, String algorithm, PrivateKey privateKey) { + byte[] sign; + try { + Signature signature = Signature.getInstance(algorithm); + signature.initSign(privateKey); + signature.update(message.getBytes(StandardCharsets.UTF_8)); + sign = signature.sign(); + } catch (NoSuchAlgorithmException e) { + throw new UnsupportedOperationException("The current Java environment does not support " + algorithm, e); + } catch (InvalidKeyException e) { + throw new IllegalArgumentException(algorithm + " signature uses an illegal privateKey.", e); + } catch (SignatureException e) { + throw new RuntimeException("An error occurred during the sign process.", e); + } + return Base64.getEncoder().encodeToString(sign); + } + + /** + * 使用公钥按照特定算法验证签名 + * + * @param message 待签名串 + * @param signature 待验证的签名内容 + * @param algorithm 签名算法,如:SHA256withRSA + * @param publicKey 验签用公钥对象 + * @return 签名验证是否通过 + */ + public static boolean verify(String message, String signature, String algorithm, + PublicKey publicKey) { + try { + Signature sign = Signature.getInstance(algorithm); + sign.initVerify(publicKey); + sign.update(message.getBytes(StandardCharsets.UTF_8)); + return sign.verify(Base64.getDecoder().decode(signature)); + } catch (SignatureException e) { + return false; + } catch (InvalidKeyException e) { + throw new IllegalArgumentException("verify uses an illegal publickey.", e); + } catch (NoSuchAlgorithmException e) { + throw new UnsupportedOperationException("The current Java environment does not support" + algorithm, e); + } + } + + /** + * 根据微信支付APIv3请求签名规则构造 Authorization 签名 + * + * @param mchid 商户号 + * @param certificateSerialNo 商户API证书序列号 + * @param privateKey 商户API证书私钥 + * @param method 请求接口的HTTP方法,请使用全大写表述,如 GET、POST、PUT、DELETE + * @param uri 请求接口的URL + * @param body 请求接口的Body + * @return 构造好的微信支付APIv3 Authorization 头 + */ + public static String buildAuthorization(String mchid, String certificateSerialNo, + PrivateKey privateKey, + String method, String uri, String body) { + String nonce = createNonce(32); + long timestamp = Instant.now().getEpochSecond(); + + String message = String.format("%s\n%s\n%d\n%s\n%s\n", method, uri, timestamp, nonce, + body == null ? "" : body); + + String signature = sign(message, "SHA256withRSA", privateKey); + + return String.format( + "WECHATPAY2-SHA256-RSA2048 mchid=\"%s\",nonce_str=\"%s\",signature=\"%s\"," + + "timestamp=\"%d\",serial_no=\"%s\"", + mchid, nonce, signature, timestamp, certificateSerialNo); + } + + /** + * 对参数进行 URL 编码 + * + * @param content 参数内容 + * @return 编码后的内容 + */ + public static String urlEncode(String content) { + try { + return URLEncoder.encode(content, StandardCharsets.UTF_8.name()); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + } + + /** + * 对参数Map进行 URL 编码,生成 QueryString + * + * @param params Query参数Map + * @return QueryString + */ + public static String urlEncode(Map params) { + if (params == null || params.isEmpty()) { + return ""; + } + + int index = 0; + StringBuilder result = new StringBuilder(); + for (Map.Entry entry : params.entrySet()) { + result.append(entry.getKey()) + .append("=") + .append(urlEncode(entry.getValue().toString())); + index++; + if (index < params.size()) { + result.append("&"); + } + } + return result.toString(); + } + + /** + * 从应答中提取 Body + * + * @param response HTTP 请求应答对象 + * @return 应答中的Body内容,Body为空时返回空字符串 + */ + public static String extractBody(Response response) { + if (response.body() == null) { + return ""; + } + + try { + BufferedSource source = response.body().source(); + return source.readUtf8(); + } catch (IOException e) { + throw new RuntimeException(String.format("An error occurred during reading response body. Status: %d", response.code()), e); + } + } + + /** + * 根据微信支付APIv3应答验签规则对应答签名进行验证,验证不通过时抛出异常 + * + * @param wechatpayPublicKeyId 微信支付公钥ID + * @param wechatpayPublicKey 微信支付公钥对象 + * @param headers 微信支付应答 Header 列表 + * @param body 微信支付应答 Body + */ + public static void validateResponse(String wechatpayPublicKeyId, PublicKey wechatpayPublicKey, + Headers headers, + String body) { + String timestamp = headers.get("Wechatpay-Timestamp"); + try { + Instant responseTime = Instant.ofEpochSecond(Long.parseLong(timestamp)); + // 拒绝过期请求 + if (Duration.between(responseTime, Instant.now()).abs().toMinutes() >= 5) { + throw new IllegalArgumentException( + String.format("Validate http response,timestamp[%s] of httpResponse is expires, " + + "request-id[%s]", + timestamp, headers.get("Request-ID"))); + } + } catch (DateTimeException | NumberFormatException e) { + throw new IllegalArgumentException( + String.format("Validate http response,timestamp[%s] of httpResponse is invalid, " + + "request-id[%s]", timestamp, + headers.get("Request-ID"))); + } + String message = String.format("%s\n%s\n%s\n", timestamp, headers.get("Wechatpay-Nonce"), + body == null ? "" : body); + String serialNumber = headers.get("Wechatpay-Serial"); + if (!Objects.equals(serialNumber, wechatpayPublicKeyId)) { + throw new IllegalArgumentException( + String.format("Invalid Wechatpay-Serial, Local: %s, Remote: %s", wechatpayPublicKeyId, + serialNumber)); + } + String signature = headers.get("Wechatpay-Signature"); + + boolean success = verify(message, signature, "SHA256withRSA", wechatpayPublicKey); + if (!success) { + throw new IllegalArgumentException( + String.format("Validate response failed,the WechatPay signature is incorrect.%n" + + "Request-ID[%s]\tresponseHeader[%s]\tresponseBody[%.1024s]", + headers.get("Request-ID"), headers, body)); + } + } + + /** + * 微信支付API错误异常,发送HTTP请求成功,但返回状态码不是 2XX 时抛出本异常 + */ + public static class ApiException extends RuntimeException { + public final int statusCode; + public final String body; + public final Headers headers; + public ApiException(int statusCode, String body, Headers headers) { + super(String.format("微信支付API访问失败,StatusCode: %s, Body: %s", statusCode, body)); + this.statusCode = statusCode; + this.body = body; + this.headers = headers; + } + } +} diff --git a/jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/demo/appletTtsPlayLog/controller/AppletTtsPlayLogController.java b/jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/demo/appletTtsPlayLog/controller/AppletTtsPlayLogController.java index c0a5e0e..55d8948 100644 --- a/jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/demo/appletTtsPlayLog/controller/AppletTtsPlayLogController.java +++ b/jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/demo/appletTtsPlayLog/controller/AppletTtsPlayLogController.java @@ -39,12 +39,12 @@ import io.swagger.v3.oas.annotations.Operation; import org.jeecg.common.aspect.annotation.AutoLog; import org.apache.shiro.authz.annotation.RequiresPermissions; /** - * @Description: 小程序语音朗读记录 + * @Description: 朗读记录 * @Author: jeecg-boot - * @Date: 2025-09-12 + * @Date: 2025-09-19 * @Version: V1.0 */ -@Tag(name="小程序语音朗读记录") +@Tag(name="朗读记录") @RestController @RequestMapping("/appletTtsPlayLog/appletTtsPlayLog") @Slf4j @@ -61,8 +61,8 @@ public class AppletTtsPlayLogController extends JeecgController> queryPageList(AppletTtsPlayLog appletTtsPlayLog, @RequestParam(name="pageNo", defaultValue="1") Integer pageNo, @@ -82,8 +82,8 @@ public class AppletTtsPlayLogController extends JeecgController add(@RequestBody AppletTtsPlayLog appletTtsPlayLog) { @@ -98,8 +98,8 @@ public class AppletTtsPlayLogController extends JeecgController edit(@RequestBody AppletTtsPlayLog appletTtsPlayLog) { @@ -113,8 +113,8 @@ public class AppletTtsPlayLogController extends JeecgController delete(@RequestParam(name="id",required=true) String id) { @@ -128,8 +128,8 @@ public class AppletTtsPlayLogController extends JeecgController deleteBatch(@RequestParam(name="ids",required=true) String ids) { @@ -143,8 +143,8 @@ public class AppletTtsPlayLogController extends JeecgController queryById(@RequestParam(name="id",required=true) String id) { AppletTtsPlayLog appletTtsPlayLog = appletTtsPlayLogService.getById(id); @@ -163,7 +163,7 @@ public class AppletTtsPlayLogController extends JeecgController { diff --git a/jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/demo/appletTtsPlayLog/service/IAppletTtsPlayLogService.java b/jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/demo/appletTtsPlayLog/service/IAppletTtsPlayLogService.java index 7e38158..21372cd 100644 --- a/jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/demo/appletTtsPlayLog/service/IAppletTtsPlayLogService.java +++ b/jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/demo/appletTtsPlayLog/service/IAppletTtsPlayLogService.java @@ -4,9 +4,9 @@ import org.jeecg.modules.demo.appletTtsPlayLog.entity.AppletTtsPlayLog; import com.baomidou.mybatisplus.extension.service.IService; /** - * @Description: 小程序语音朗读记录 + * @Description: 朗读记录 * @Author: jeecg-boot - * @Date: 2025-09-12 + * @Date: 2025-09-19 * @Version: V1.0 */ public interface IAppletTtsPlayLogService extends IService { diff --git a/jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/demo/appletTtsPlayLog/service/impl/AppletTtsPlayLogServiceImpl.java b/jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/demo/appletTtsPlayLog/service/impl/AppletTtsPlayLogServiceImpl.java index 2b67eb8..7f86901 100644 --- a/jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/demo/appletTtsPlayLog/service/impl/AppletTtsPlayLogServiceImpl.java +++ b/jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/demo/appletTtsPlayLog/service/impl/AppletTtsPlayLogServiceImpl.java @@ -8,9 +8,9 @@ import org.springframework.stereotype.Service; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; /** - * @Description: 小程序语音朗读记录 + * @Description: 朗读记录 * @Author: jeecg-boot - * @Date: 2025-09-12 + * @Date: 2025-09-19 * @Version: V1.0 */ @Service diff --git a/jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/demo/appletTtsPlayLog/uniapp/AppletTtsPlayLogForm.vue b/jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/demo/appletTtsPlayLog/uniapp/AppletTtsPlayLogForm.vue index f358429..746e18a 100644 --- a/jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/demo/appletTtsPlayLog/uniapp/AppletTtsPlayLogForm.vue +++ b/jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/demo/appletTtsPlayLog/uniapp/AppletTtsPlayLogForm.vue @@ -3,7 +3,7 @@ 返回 - 小程序语音朗读记录 + 朗读记录 @@ -22,8 +22,8 @@ - 音色: - + 音色ID: + @@ -38,7 +38,6 @@ - 耗时: diff --git a/jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/demo/appletTtsPlayLog/uniapp/AppletTtsPlayLogList.vue b/jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/demo/appletTtsPlayLog/uniapp/AppletTtsPlayLogList.vue index 86ac778..53e1724 100644 --- a/jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/demo/appletTtsPlayLog/uniapp/AppletTtsPlayLogList.vue +++ b/jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/demo/appletTtsPlayLog/uniapp/AppletTtsPlayLogList.vue @@ -3,7 +3,7 @@ 返回 - 小程序语音朗读记录 + 朗读记录 @@ -25,7 +25,7 @@ import Mixin from "@/common/mixin/Mixin.js"; export default { - name: '小程序语音朗读记录', + name: '朗读记录', mixins: [MescrollMixin,Mixin], data() { return { diff --git a/jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/demo/appletTtsPlayLog/uniapp3/AppletTtsPlayLogData.ts b/jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/demo/appletTtsPlayLog/uniapp3/AppletTtsPlayLogData.ts index e1972e6..ad4a06e 100644 --- a/jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/demo/appletTtsPlayLog/uniapp3/AppletTtsPlayLogData.ts +++ b/jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/demo/appletTtsPlayLog/uniapp3/AppletTtsPlayLogData.ts @@ -12,7 +12,7 @@ export const columns = [ dataIndex: 'text' }, { - title: '音色', + title: '音色ID', align:"center", dataIndex: 'voicetype' }, @@ -26,11 +26,6 @@ export const columns = [ align:"center", dataIndex: 'speed' }, - { - title: '创建日期', - align:"center", - dataIndex: 'createTime' - }, { title: '耗时', align:"center", @@ -39,9 +34,6 @@ export const columns = [ { title: '是否成功', align:"center", - dataIndex: 'success', - customRender:({text}) => { - return render.renderSwitch(text, [{text:'是',value:'Y'},{text:'否',value:'N'}]) - }, + dataIndex: 'success' }, ]; \ No newline at end of file diff --git a/jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/demo/appletTtsPlayLog/uniapp3/AppletTtsPlayLogForm.vue b/jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/demo/appletTtsPlayLog/uniapp3/AppletTtsPlayLogForm.vue index 1333f26..9a41bd1 100644 --- a/jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/demo/appletTtsPlayLog/uniapp3/AppletTtsPlayLogForm.vue +++ b/jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/demo/appletTtsPlayLog/uniapp3/AppletTtsPlayLogForm.vue @@ -3,7 +3,7 @@ layout: 'default', style: { navigationStyle: 'custom', -navigationBarTitleText: '小程序语音朗读记录', +navigationBarTitleText: '朗读记录', }, } @@ -43,10 +43,10 @@ navigationBarTitleText: '小程序语音朗读记录', - - - - - - + - + prop='success' + placeholder="请选择是否成功" + :rules="[ + ]" + clearable + /> diff --git a/jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/demo/appletTtsPlayLog/uniapp3/AppletTtsPlayLogList.vue b/jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/demo/appletTtsPlayLog/uniapp3/AppletTtsPlayLogList.vue index e8a5a5c..ec72047 100644 --- a/jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/demo/appletTtsPlayLog/uniapp3/AppletTtsPlayLogList.vue +++ b/jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/demo/appletTtsPlayLog/uniapp3/AppletTtsPlayLogList.vue @@ -2,13 +2,13 @@ { layout: 'default', style: { -navigationBarTitleText: '小程序语音朗读记录', +navigationBarTitleText: '朗读记录', navigationStyle: 'custom', }, }