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