| @ -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<StatisticsItem> getDashboardStatistics() { | |||||
| try { | |||||
| List<StatisticsItem> 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<AppletUser> 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<AppletUser> 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.<AppletUser>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; | |||||
| } | |||||
| } | |||||
| } | |||||
| @ -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; | |||||
| } | |||||
| } | |||||
| @ -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<TransferSceneReportInfo> transferSceneReportInfos; | |||||
| } | |||||
| } | |||||
| @ -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> T fromJson(String json, Class<T> 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<String, Object> params) { | |||||
| if (params == null || params.isEmpty()) { | |||||
| return ""; | |||||
| } | |||||
| int index = 0; | |||||
| StringBuilder result = new StringBuilder(); | |||||
| for (Map.Entry<String, Object> 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; | |||||
| } | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,64 @@ | |||||
| import {defHttp} from '/@/utils/http/axios'; | |||||
| import { useMessage } from "/@/hooks/web/useMessage"; | |||||
| const { createConfirm } = useMessage(); | |||||
| enum Api { | |||||
| list = '/appletWithdrawal/appletWithdrawal/list', | |||||
| save='/appletWithdrawal/appletWithdrawal/add', | |||||
| edit='/appletWithdrawal/appletWithdrawal/edit', | |||||
| deleteOne = '/appletWithdrawal/appletWithdrawal/delete', | |||||
| deleteBatch = '/appletWithdrawal/appletWithdrawal/deleteBatch', | |||||
| importExcel = '/appletWithdrawal/appletWithdrawal/importExcel', | |||||
| exportXls = '/appletWithdrawal/appletWithdrawal/exportXls', | |||||
| } | |||||
| /** | |||||
| * 导出api | |||||
| * @param params | |||||
| */ | |||||
| export const getExportUrl = Api.exportXls; | |||||
| /** | |||||
| * 导入api | |||||
| */ | |||||
| export const getImportUrl = Api.importExcel; | |||||
| /** | |||||
| * 列表接口 | |||||
| * @param params | |||||
| */ | |||||
| export const list = (params) => | |||||
| defHttp.get({url: Api.list, params}); | |||||
| /** | |||||
| * 删除单个 | |||||
| */ | |||||
| export const deleteOne = (params,handleSuccess) => { | |||||
| return defHttp.delete({url: Api.deleteOne, params}, {joinParamsToUrl: true}).then(() => { | |||||
| handleSuccess(); | |||||
| }); | |||||
| } | |||||
| /** | |||||
| * 批量删除 | |||||
| * @param params | |||||
| */ | |||||
| export const batchDelete = (params, handleSuccess) => { | |||||
| createConfirm({ | |||||
| iconType: 'warning', | |||||
| title: '确认删除', | |||||
| content: '是否删除选中数据', | |||||
| okText: '确认', | |||||
| cancelText: '取消', | |||||
| onOk: () => { | |||||
| return defHttp.delete({url: Api.deleteBatch, data: params}, {joinParamsToUrl: true}).then(() => { | |||||
| handleSuccess(); | |||||
| }); | |||||
| } | |||||
| }); | |||||
| } | |||||
| /** | |||||
| * 保存或者更新 | |||||
| * @param params | |||||
| */ | |||||
| export const saveOrUpdate = (params, isUpdate) => { | |||||
| let url = isUpdate ? Api.edit : Api.save; | |||||
| return defHttp.post({url: url, params}); | |||||
| } | |||||
| @ -0,0 +1,142 @@ | |||||
| import {BasicColumn} from '/@/components/Table'; | |||||
| import {FormSchema} from '/@/components/Table'; | |||||
| import { rules} from '/@/utils/helper/validator'; | |||||
| import { render } from '/@/utils/common/renderUtils'; | |||||
| import { getWeekMonthQuarterYear } from '/@/utils'; | |||||
| //列表数据 | |||||
| export const columns: BasicColumn[] = [ | |||||
| { | |||||
| title: '用户id', | |||||
| align:"center", | |||||
| dataIndex: 'userId_dictText' | |||||
| }, | |||||
| { | |||||
| title: '申请人', | |||||
| align:"center", | |||||
| dataIndex: 'name' | |||||
| }, | |||||
| { | |||||
| title: '提现金额', | |||||
| align:"center", | |||||
| dataIndex: 'money' | |||||
| }, | |||||
| { | |||||
| title: '提现方式', | |||||
| align:"center", | |||||
| dataIndex: 'method_dictText' | |||||
| }, | |||||
| { | |||||
| title: '凭证上传', | |||||
| align:"center", | |||||
| dataIndex: 'upload', | |||||
| customRender:({text}) => { | |||||
| return render.renderSwitch(text, [{text:'是',value:'Y'},{text:'否',value:'N'}]) | |||||
| }, | |||||
| }, | |||||
| { | |||||
| title: '审核状态', | |||||
| align:"center", | |||||
| dataIndex: 'status_dictText' | |||||
| }, | |||||
| { | |||||
| title: '提现状态', | |||||
| align:"center", | |||||
| dataIndex: 'withdrawStatus_dictText' | |||||
| }, | |||||
| { | |||||
| title: '流水号', | |||||
| align:"center", | |||||
| dataIndex: 'waterId_dictText' | |||||
| }, | |||||
| ]; | |||||
| //查询数据 | |||||
| export const searchFormSchema: FormSchema[] = [ | |||||
| ]; | |||||
| //表单数据 | |||||
| export const formSchema: FormSchema[] = [ | |||||
| { | |||||
| label: '用户id', | |||||
| field: 'userId', | |||||
| component: 'JSearchSelect', | |||||
| componentProps:{ | |||||
| dict:"applet_user,name,id" | |||||
| }, | |||||
| }, | |||||
| { | |||||
| label: '申请人', | |||||
| field: 'name', | |||||
| component: 'Input', | |||||
| }, | |||||
| { | |||||
| label: '提现金额', | |||||
| field: 'money', | |||||
| component: 'InputNumber', | |||||
| }, | |||||
| { | |||||
| label: '提现方式', | |||||
| field: 'method', | |||||
| component: 'JDictSelectTag', | |||||
| componentProps:{ | |||||
| dictCode:"applett_translate_type" | |||||
| }, | |||||
| }, | |||||
| { | |||||
| label: '凭证上传', | |||||
| field: 'upload', | |||||
| component: 'JSwitch', | |||||
| componentProps:{ | |||||
| }, | |||||
| }, | |||||
| { | |||||
| label: '审核状态', | |||||
| field: 'status', | |||||
| component: 'JDictSelectTag', | |||||
| componentProps:{ | |||||
| dictCode:"applett_money_type" | |||||
| }, | |||||
| }, | |||||
| { | |||||
| label: '提现状态', | |||||
| field: 'withdrawStatus', | |||||
| component: 'JDictSelectTag', | |||||
| componentProps:{ | |||||
| dictCode:"applet_withdraw_type" | |||||
| }, | |||||
| }, | |||||
| { | |||||
| label: '流水号', | |||||
| field: 'waterId', | |||||
| component: 'JSearchSelect', | |||||
| componentProps:{ | |||||
| dict:"applet_water,number,id" | |||||
| }, | |||||
| }, | |||||
| // TODO 主键隐藏字段,目前写死为ID | |||||
| { | |||||
| label: '', | |||||
| field: 'id', | |||||
| component: 'Input', | |||||
| show: false | |||||
| }, | |||||
| ]; | |||||
| // 高级查询数据 | |||||
| export const superQuerySchema = { | |||||
| userId: {title: '用户id',order: 0,view: 'sel_search', type: 'string',dictTable: "applet_user", dictCode: 'id', dictText: 'name',}, | |||||
| name: {title: '申请人',order: 1,view: 'text', type: 'string',}, | |||||
| money: {title: '提现金额',order: 2,view: 'number', type: 'number',}, | |||||
| method: {title: '提现方式',order: 3,view: 'list', type: 'string',dictCode: 'applett_translate_type',}, | |||||
| upload: {title: '凭证上传',order: 4,view: 'switch', type: 'string',}, | |||||
| status: {title: '审核状态',order: 5,view: 'list', type: 'string',dictCode: 'applett_money_type',}, | |||||
| withdrawStatus: {title: '提现状态',order: 6,view: 'list', type: 'string',dictCode: 'applet_withdraw_type',}, | |||||
| waterId: {title: '流水号',order: 7,view: 'sel_search', type: 'string',dictTable: "applet_water", dictCode: 'id', dictText: 'number',}, | |||||
| }; | |||||
| /** | |||||
| * 流程表单调用这个方法获取formSchema | |||||
| * @param param | |||||
| */ | |||||
| export function getBpmFormSchema(_formData): FormSchema[]{ | |||||
| // 默认和原始表单保持一致 如果流程中配置了权限数据,这里需要单独处理formSchema | |||||
| return formSchema; | |||||
| } | |||||
| @ -0,0 +1,206 @@ | |||||
| <template> | |||||
| <div> | |||||
| <!--引用表格--> | |||||
| <BasicTable @register="registerTable" :rowSelection="rowSelection"> | |||||
| <!--插槽:table标题--> | |||||
| <template #tableTitle> | |||||
| <a-button type="primary" v-auth="'appletWithdrawal:applet_withdrawal:add'" @click="handleAdd" preIcon="ant-design:plus-outlined"> 新增</a-button> | |||||
| <a-button type="primary" v-auth="'appletWithdrawal:applet_withdrawal:exportXls'" preIcon="ant-design:export-outlined" @click="onExportXls"> 导出</a-button> | |||||
| <j-upload-button type="primary" v-auth="'appletWithdrawal:applet_withdrawal:importExcel'" preIcon="ant-design:import-outlined" @click="onImportXls">导入</j-upload-button> | |||||
| <a-dropdown v-if="selectedRowKeys.length > 0"> | |||||
| <template #overlay> | |||||
| <a-menu> | |||||
| <a-menu-item key="1" @click="batchHandleDelete"> | |||||
| <Icon icon="ant-design:delete-outlined"></Icon> | |||||
| 删除 | |||||
| </a-menu-item> | |||||
| </a-menu> | |||||
| </template> | |||||
| <a-button v-auth="'appletWithdrawal:applet_withdrawal:deleteBatch'">批量操作 | |||||
| <Icon icon="mdi:chevron-down"></Icon> | |||||
| </a-button> | |||||
| </a-dropdown> | |||||
| <!-- 高级查询 --> | |||||
| <super-query :config="superQueryConfig" @search="handleSuperQuery" /> | |||||
| </template> | |||||
| <!--操作栏--> | |||||
| <template #action="{ record }"> | |||||
| <TableAction :actions="getTableAction(record)" :dropDownActions="getDropDownAction(record)"/> | |||||
| </template> | |||||
| <!--字段回显插槽--> | |||||
| <template v-slot:bodyCell="{ column, record, index, text }"> | |||||
| </template> | |||||
| </BasicTable> | |||||
| <!-- 表单区域 --> | |||||
| <AppletWithdrawalModal @register="registerModal" @success="handleSuccess"></AppletWithdrawalModal> | |||||
| </div> | |||||
| </template> | |||||
| <script lang="ts" name="appletWithdrawal-appletWithdrawal" setup> | |||||
| import {ref, reactive, computed, unref} from 'vue'; | |||||
| import {BasicTable, useTable, TableAction} from '/@/components/Table'; | |||||
| import {useModal} from '/@/components/Modal'; | |||||
| import { useListPage } from '/@/hooks/system/useListPage' | |||||
| import AppletWithdrawalModal from './components/AppletWithdrawalModal.vue' | |||||
| import {columns, searchFormSchema, superQuerySchema} from './AppletWithdrawal.data'; | |||||
| import {list, deleteOne, batchDelete, getImportUrl,getExportUrl} from './AppletWithdrawal.api'; | |||||
| import { downloadFile } from '/@/utils/common/renderUtils'; | |||||
| import { useUserStore } from '/@/store/modules/user'; | |||||
| import { useMessage } from '/@/hooks/web/useMessage'; | |||||
| import { getDateByPicker } from '/@/utils'; | |||||
| //日期个性化选择 | |||||
| const fieldPickers = reactive({ | |||||
| }); | |||||
| const queryParam = reactive<any>({}); | |||||
| const checkedKeys = ref<Array<string | number>>([]); | |||||
| const userStore = useUserStore(); | |||||
| const { createMessage } = useMessage(); | |||||
| //注册model | |||||
| const [registerModal, {openModal}] = useModal(); | |||||
| //注册table数据 | |||||
| const { prefixCls,tableContext,onExportXls,onImportXls } = useListPage({ | |||||
| tableProps:{ | |||||
| title: '提现', | |||||
| api: list, | |||||
| columns, | |||||
| canResize:true, | |||||
| formConfig: { | |||||
| //labelWidth: 120, | |||||
| schemas: searchFormSchema, | |||||
| autoSubmitOnEnter:true, | |||||
| showAdvancedButton:true, | |||||
| fieldMapToNumber: [ | |||||
| ], | |||||
| fieldMapToTime: [ | |||||
| ], | |||||
| }, | |||||
| actionColumn: { | |||||
| width: 120, | |||||
| fixed:'right' | |||||
| }, | |||||
| beforeFetch: (params) => { | |||||
| if (params && fieldPickers) { | |||||
| for (let key in fieldPickers) { | |||||
| if (params[key]) { | |||||
| params[key] = getDateByPicker(params[key], fieldPickers[key]); | |||||
| } | |||||
| } | |||||
| } | |||||
| return Object.assign(params, queryParam); | |||||
| }, | |||||
| }, | |||||
| exportConfig: { | |||||
| name:"提现", | |||||
| url: getExportUrl, | |||||
| params: queryParam, | |||||
| }, | |||||
| importConfig: { | |||||
| url: getImportUrl, | |||||
| success: handleSuccess | |||||
| }, | |||||
| }) | |||||
| const [registerTable, {reload},{ rowSelection, selectedRowKeys }] = tableContext | |||||
| // 高级查询配置 | |||||
| const superQueryConfig = reactive(superQuerySchema); | |||||
| /** | |||||
| * 高级查询事件 | |||||
| */ | |||||
| function handleSuperQuery(params) { | |||||
| Object.keys(params).map((k) => { | |||||
| queryParam[k] = params[k]; | |||||
| }); | |||||
| reload(); | |||||
| } | |||||
| /** | |||||
| * 新增事件 | |||||
| */ | |||||
| function handleAdd() { | |||||
| openModal(true, { | |||||
| isUpdate: false, | |||||
| showFooter: true, | |||||
| }); | |||||
| } | |||||
| /** | |||||
| * 编辑事件 | |||||
| */ | |||||
| function handleEdit(record: Recordable) { | |||||
| openModal(true, { | |||||
| record, | |||||
| isUpdate: true, | |||||
| showFooter: true, | |||||
| }); | |||||
| } | |||||
| /** | |||||
| * 详情 | |||||
| */ | |||||
| function handleDetail(record: Recordable) { | |||||
| openModal(true, { | |||||
| record, | |||||
| isUpdate: true, | |||||
| showFooter: false, | |||||
| }); | |||||
| } | |||||
| /** | |||||
| * 删除事件 | |||||
| */ | |||||
| async function handleDelete(record) { | |||||
| await deleteOne({id: record.id}, handleSuccess); | |||||
| } | |||||
| /** | |||||
| * 批量删除事件 | |||||
| */ | |||||
| async function batchHandleDelete() { | |||||
| await batchDelete({ids: selectedRowKeys.value}, handleSuccess); | |||||
| } | |||||
| /** | |||||
| * 成功回调 | |||||
| */ | |||||
| function handleSuccess() { | |||||
| (selectedRowKeys.value = []) && reload(); | |||||
| } | |||||
| /** | |||||
| * 操作栏 | |||||
| */ | |||||
| function getTableAction(record){ | |||||
| return [ | |||||
| { | |||||
| label: '编辑', | |||||
| onClick: handleEdit.bind(null, record), | |||||
| auth: 'appletWithdrawal:applet_withdrawal:edit' | |||||
| } | |||||
| ] | |||||
| } | |||||
| /** | |||||
| * 下拉操作栏 | |||||
| */ | |||||
| function getDropDownAction(record){ | |||||
| return [ | |||||
| { | |||||
| label: '详情', | |||||
| onClick: handleDetail.bind(null, record), | |||||
| }, { | |||||
| label: '删除', | |||||
| popConfirm: { | |||||
| title: '是否确认删除', | |||||
| confirm: handleDelete.bind(null, record), | |||||
| placement: 'topLeft', | |||||
| }, | |||||
| auth: 'appletWithdrawal:applet_withdrawal:delete' | |||||
| } | |||||
| ] | |||||
| } | |||||
| </script> | |||||
| <style lang="less" scoped> | |||||
| :deep(.ant-picker),:deep(.ant-input-number){ | |||||
| width: 100%; | |||||
| } | |||||
| </style> | |||||
| @ -0,0 +1,26 @@ | |||||
| -- 注意:该页面对应的前台目录为views/appletWithdrawal文件夹下 | |||||
| -- 如果你想更改到其他目录,请修改sql中component字段对应的值 | |||||
| INSERT INTO sys_permission(id, parent_id, name, url, component, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_route, is_leaf, keep_alive, hidden, hide_tab, description, status, del_flag, rule_flag, create_by, create_time, update_by, update_time, internal_or_external) | |||||
| VALUES ('2025091903554910460', NULL, '提现', '/appletWithdrawal/appletWithdrawalList', 'appletWithdrawal/AppletWithdrawalList', NULL, NULL, 0, NULL, '1', 0.00, 0, NULL, 1, 0, 0, 0, 0, NULL, '1', 0, 0, 'admin', '2025-09-19 15:55:46', NULL, NULL, 0); | |||||
| -- 权限控制sql | |||||
| -- 新增 | |||||
| INSERT INTO sys_permission(id, parent_id, name, url, component, is_route, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_leaf, keep_alive, hidden, hide_tab, description, create_by, create_time, update_by, update_time, del_flag, rule_flag, status, internal_or_external) | |||||
| VALUES ('2025091903554910461', '2025091903554910460', '添加提现', NULL, NULL, 0, NULL, NULL, 2, 'appletWithdrawal:applet_withdrawal:add', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2025-09-19 15:55:46', NULL, NULL, 0, 0, '1', 0); | |||||
| -- 编辑 | |||||
| INSERT INTO sys_permission(id, parent_id, name, url, component, is_route, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_leaf, keep_alive, hidden, hide_tab, description, create_by, create_time, update_by, update_time, del_flag, rule_flag, status, internal_or_external) | |||||
| VALUES ('2025091903554910462', '2025091903554910460', '编辑提现', NULL, NULL, 0, NULL, NULL, 2, 'appletWithdrawal:applet_withdrawal:edit', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2025-09-19 15:55:46', NULL, NULL, 0, 0, '1', 0); | |||||
| -- 删除 | |||||
| INSERT INTO sys_permission(id, parent_id, name, url, component, is_route, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_leaf, keep_alive, hidden, hide_tab, description, create_by, create_time, update_by, update_time, del_flag, rule_flag, status, internal_or_external) | |||||
| VALUES ('2025091903554910463', '2025091903554910460', '删除提现', NULL, NULL, 0, NULL, NULL, 2, 'appletWithdrawal:applet_withdrawal:delete', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2025-09-19 15:55:46', NULL, NULL, 0, 0, '1', 0); | |||||
| -- 批量删除 | |||||
| INSERT INTO sys_permission(id, parent_id, name, url, component, is_route, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_leaf, keep_alive, hidden, hide_tab, description, create_by, create_time, update_by, update_time, del_flag, rule_flag, status, internal_or_external) | |||||
| VALUES ('2025091903554910464', '2025091903554910460', '批量删除提现', NULL, NULL, 0, NULL, NULL, 2, 'appletWithdrawal:applet_withdrawal:deleteBatch', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2025-09-19 15:55:46', NULL, NULL, 0, 0, '1', 0); | |||||
| -- 导出excel | |||||
| INSERT INTO sys_permission(id, parent_id, name, url, component, is_route, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_leaf, keep_alive, hidden, hide_tab, description, create_by, create_time, update_by, update_time, del_flag, rule_flag, status, internal_or_external) | |||||
| VALUES ('2025091903554910465', '2025091903554910460', '导出excel_提现', NULL, NULL, 0, NULL, NULL, 2, 'appletWithdrawal:applet_withdrawal:exportXls', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2025-09-19 15:55:46', NULL, NULL, 0, 0, '1', 0); | |||||
| -- 导入excel | |||||
| INSERT INTO sys_permission(id, parent_id, name, url, component, is_route, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_leaf, keep_alive, hidden, hide_tab, description, create_by, create_time, update_by, update_time, del_flag, rule_flag, status, internal_or_external) | |||||
| VALUES ('2025091903554910466', '2025091903554910460', '导入excel_提现', NULL, NULL, 0, NULL, NULL, 2, 'appletWithdrawal:applet_withdrawal:importExcel', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2025-09-19 15:55:46', NULL, NULL, 0, 0, '1', 0); | |||||
| @ -0,0 +1,70 @@ | |||||
| <template> | |||||
| <div style="min-height: 400px"> | |||||
| <BasicForm @register="registerForm"></BasicForm> | |||||
| <div style="width: 100%;text-align: center" v-if="!formDisabled"> | |||||
| <a-button @click="submitForm" pre-icon="ant-design:check" type="primary">提 交</a-button> | |||||
| </div> | |||||
| </div> | |||||
| </template> | |||||
| <script lang="ts"> | |||||
| import {BasicForm, useForm} from '/@/components/Form/index'; | |||||
| import {computed, defineComponent} from 'vue'; | |||||
| import {defHttp} from '/@/utils/http/axios'; | |||||
| import { propTypes } from '/@/utils/propTypes'; | |||||
| import {getBpmFormSchema} from '../AppletWithdrawal.data'; | |||||
| import {saveOrUpdate} from '../AppletWithdrawal.api'; | |||||
| export default defineComponent({ | |||||
| name: "AppletWithdrawalForm", | |||||
| components:{ | |||||
| BasicForm | |||||
| }, | |||||
| props:{ | |||||
| formData: propTypes.object.def({}), | |||||
| formBpm: propTypes.bool.def(true), | |||||
| }, | |||||
| setup(props){ | |||||
| const [registerForm, { setFieldsValue, setProps, getFieldsValue }] = useForm({ | |||||
| labelWidth: 150, | |||||
| schemas: getBpmFormSchema(props.formData), | |||||
| showActionButtonGroup: false, | |||||
| baseColProps: {span: 24} | |||||
| }); | |||||
| const formDisabled = computed(()=>{ | |||||
| if(props.formData.disabled === false){ | |||||
| return false; | |||||
| } | |||||
| return true; | |||||
| }); | |||||
| let formData = {}; | |||||
| const queryByIdUrl = '/appletWithdrawal/appletWithdrawal/queryById'; | |||||
| async function initFormData(){ | |||||
| let params = {id: props.formData.dataId}; | |||||
| const data = await defHttp.get({url: queryByIdUrl, params}); | |||||
| formData = {...data} | |||||
| //设置表单的值 | |||||
| await setFieldsValue(formData); | |||||
| //默认是禁用 | |||||
| await setProps({disabled: formDisabled.value}) | |||||
| } | |||||
| async function submitForm() { | |||||
| let data = getFieldsValue(); | |||||
| let params = Object.assign({}, formData, data); | |||||
| console.log('表单数据', params) | |||||
| await saveOrUpdate(params, true) | |||||
| } | |||||
| initFormData(); | |||||
| return { | |||||
| registerForm, | |||||
| formDisabled, | |||||
| submitForm, | |||||
| } | |||||
| } | |||||
| }); | |||||
| </script> | |||||
| @ -0,0 +1,99 @@ | |||||
| <template> | |||||
| <BasicModal v-bind="$attrs" @register="registerModal" destroyOnClose :title="title" :width="800" @ok="handleSubmit"> | |||||
| <BasicForm @register="registerForm" name="AppletWithdrawalForm" /> | |||||
| </BasicModal> | |||||
| </template> | |||||
| <script lang="ts" setup> | |||||
| import {ref, computed, unref, reactive} from 'vue'; | |||||
| import {BasicModal, useModalInner} from '/@/components/Modal'; | |||||
| import {BasicForm, useForm} from '/@/components/Form/index'; | |||||
| import {formSchema} from '../AppletWithdrawal.data'; | |||||
| import {saveOrUpdate} from '../AppletWithdrawal.api'; | |||||
| import { useMessage } from '/@/hooks/web/useMessage'; | |||||
| import { getDateByPicker } from '/@/utils'; | |||||
| const { createMessage } = useMessage(); | |||||
| // Emits声明 | |||||
| const emit = defineEmits(['register','success']); | |||||
| const isUpdate = ref(true); | |||||
| const isDetail = ref(false); | |||||
| //表单配置 | |||||
| const [registerForm, { setProps,resetFields, setFieldsValue, validate, scrollToField }] = useForm({ | |||||
| labelWidth: 150, | |||||
| schemas: formSchema, | |||||
| showActionButtonGroup: false, | |||||
| baseColProps: {span: 24} | |||||
| }); | |||||
| //表单赋值 | |||||
| const [registerModal, {setModalProps, closeModal}] = useModalInner(async (data) => { | |||||
| //重置表单 | |||||
| await resetFields(); | |||||
| setModalProps({confirmLoading: false,showCancelBtn:!!data?.showFooter,showOkBtn:!!data?.showFooter}); | |||||
| isUpdate.value = !!data?.isUpdate; | |||||
| isDetail.value = !!data?.showFooter; | |||||
| if (unref(isUpdate)) { | |||||
| //表单赋值 | |||||
| await setFieldsValue({ | |||||
| ...data.record, | |||||
| }); | |||||
| } | |||||
| // 隐藏底部时禁用整个表单 | |||||
| setProps({ disabled: !data?.showFooter }) | |||||
| }); | |||||
| //日期个性化选择 | |||||
| const fieldPickers = reactive({ | |||||
| }); | |||||
| //设置标题 | |||||
| const title = computed(() => (!unref(isUpdate) ? '新增' : !unref(isDetail) ? '详情' : '编辑')); | |||||
| //表单提交事件 | |||||
| async function handleSubmit(v) { | |||||
| try { | |||||
| let values = await validate(); | |||||
| // 预处理日期数据 | |||||
| changeDateValue(values); | |||||
| setModalProps({confirmLoading: true}); | |||||
| //提交表单 | |||||
| await saveOrUpdate(values, isUpdate.value); | |||||
| //关闭弹窗 | |||||
| closeModal(); | |||||
| //刷新列表 | |||||
| emit('success'); | |||||
| } catch ({ errorFields }) { | |||||
| if (errorFields) { | |||||
| const firstField = errorFields[0]; | |||||
| if (firstField) { | |||||
| scrollToField(firstField.name, { behavior: 'smooth', block: 'center' }); | |||||
| } | |||||
| } | |||||
| return Promise.reject(errorFields); | |||||
| } finally { | |||||
| setModalProps({confirmLoading: false}); | |||||
| } | |||||
| } | |||||
| /** | |||||
| * 处理日期值 | |||||
| * @param formData 表单数据 | |||||
| */ | |||||
| const changeDateValue = (formData) => { | |||||
| if (formData && fieldPickers) { | |||||
| for (let key in fieldPickers) { | |||||
| if (formData[key]) { | |||||
| formData[key] = getDateByPicker(formData[key], fieldPickers[key]); | |||||
| } | |||||
| } | |||||
| } | |||||
| }; | |||||
| </script> | |||||
| <style lang="less" scoped> | |||||
| /** 时间和数字输入框样式 */ | |||||
| :deep(.ant-input-number) { | |||||
| width: 100%; | |||||
| } | |||||
| :deep(.ant-calendar-picker) { | |||||
| width: 100%; | |||||
| } | |||||
| </style> | |||||