diff --git a/_config/sql/update-tables.sql b/_config/sql/update-tables.sql index b162db3f0..22b34426f 100644 --- a/_config/sql/update-tables.sql +++ b/_config/sql/update-tables.sql @@ -1,5 +1,47 @@ -- ==================================================================== -- 增量表结构脚本(PostgreSQL) -- 升级顺序:先本文件,再 update-datas.sql --- 当前已清空,后续增量 DDL 追加在此 -- ==================================================================== + +-- ---------------------------- +-- Table structure for pay_blacklist +-- 支付黑名单(平台级;IP全局 / 支付宝userId全局 / 微信按平台应用) +-- ---------------------------- +DROP TABLE IF EXISTS "public"."pay_blacklist"; +CREATE TABLE "public"."pay_blacklist" ( + "id" int8 NOT NULL, + "creator" int8, + "create_time" timestamptz(6), + "last_modifier" int8, + "last_modified_time" timestamptz(6), + "version" int4 DEFAULT 0, + "deleted" bool DEFAULT false, + "type" varchar(32) COLLATE "pg_catalog"."default" NOT NULL, + "value" varchar(128) COLLATE "pg_catalog"."default" NOT NULL, + "wx_app_id" varchar(64) COLLATE "pg_catalog"."default", + "status" varchar(16) COLLATE "pg_catalog"."default" NOT NULL DEFAULT 'enable', + "reason" varchar(255) COLLATE "pg_catalog"."default", + "expire_time" timestamptz(6), + "remark" varchar(255) COLLATE "pg_catalog"."default" +) +; +COMMENT ON TABLE "public"."pay_blacklist" IS '支付黑名单(平台级;IP全局 / 支付宝userId全局 / 微信按平台应用)'; +COMMENT ON COLUMN "public"."pay_blacklist"."id" IS '主键'; +COMMENT ON COLUMN "public"."pay_blacklist"."creator" IS '创建者ID'; +COMMENT ON COLUMN "public"."pay_blacklist"."create_time" IS '创建时间'; +COMMENT ON COLUMN "public"."pay_blacklist"."last_modifier" IS '最后修改ID'; +COMMENT ON COLUMN "public"."pay_blacklist"."last_modified_time" IS '最后修改时间'; +COMMENT ON COLUMN "public"."pay_blacklist"."version" IS '版本号'; +COMMENT ON COLUMN "public"."pay_blacklist"."deleted" IS '删除标志'; +COMMENT ON COLUMN "public"."pay_blacklist"."type" IS '名单类型: ip / alipay_user / wechat_openid'; +COMMENT ON COLUMN "public"."pay_blacklist"."value" IS '名单值(IP、支付宝userId或微信openId)'; +COMMENT ON COLUMN "public"."pay_blacklist"."wx_app_id" IS '微信平台支付应用 AppId;仅 wechat_openid 使用'; +COMMENT ON COLUMN "public"."pay_blacklist"."status" IS '状态: enable-启用, disable-禁用'; +COMMENT ON COLUMN "public"."pay_blacklist"."reason" IS '拉黑原因'; +COMMENT ON COLUMN "public"."pay_blacklist"."expire_time" IS '过期时间(空表示永久有效)'; +COMMENT ON COLUMN "public"."pay_blacklist"."remark" IS '备注'; + +ALTER TABLE "public"."pay_blacklist" ADD CONSTRAINT "pay_blacklist_pkey" PRIMARY KEY ("id"); +CREATE INDEX "idx_pay_blacklist_type_value" ON "public"."pay_blacklist" USING btree ("type", "value"); +CREATE INDEX "idx_pay_blacklist_status" ON "public"."pay_blacklist" USING btree ("status"); +CREATE UNIQUE INDEX "uk_pay_blacklist_type_value_app" ON "public"."pay_blacklist" USING btree ("type", "value", (COALESCE("wx_app_id", ''))); diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/enum/pay_blacklist_type.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/enum/pay_blacklist_type.json index a8e24b04a..55767a83a 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/enum/pay_blacklist_type.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/enum/pay_blacklist_type.json @@ -1,4 +1,5 @@ { "ip": "IP", - "open_id": "OpenId" + "alipay_user": "Alipay user", + "wechat_openid": "WeChat OpenId" } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/pay/error.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/pay/error.json index 78b34edd2..4955ddfe4 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/pay/error.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/pay/error.json @@ -132,6 +132,8 @@ "blacklistNotFound": "Blacklist entry not found", "blacklistTypeInvalid": "Invalid blacklist type", "blacklistStatusInvalid": "Invalid blacklist status", + "blacklistWechatAppRequired": "WeChat OpenId blacklist requires a payment app", + "blacklistChannelInvalid": "Invalid blacklist channel", "hitNotFound": "Risk hit record not found", "handleStatusInvalid": "Invalid handle status" } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/id-ID/enum/pay_blacklist_type.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/id-ID/enum/pay_blacklist_type.json index a8e24b04a..ad4167299 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/id-ID/enum/pay_blacklist_type.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/id-ID/enum/pay_blacklist_type.json @@ -1,4 +1,5 @@ { "ip": "IP", - "open_id": "OpenId" + "alipay_user": "Pengguna Alipay", + "wechat_openid": "OpenId WeChat" } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/id-ID/pay/error.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/id-ID/pay/error.json index 04237a150..6643615ba 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/id-ID/pay/error.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/id-ID/pay/error.json @@ -131,6 +131,8 @@ "blacklistNotFound": "Entri daftar hitam tidak ditemukan", "blacklistTypeInvalid": "Jenis daftar hitam tidak valid", "blacklistStatusInvalid": "Status daftar hitam tidak valid", + "blacklistWechatAppRequired": "Blacklist OpenId WeChat wajib memilih aplikasi pembayaran", + "blacklistChannelInvalid": "Kanal daftar hitam tidak valid", "hitNotFound": "Catatan hit risiko tidak ditemukan", "handleStatusInvalid": "Status penanganan tidak valid" } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ja-JP/enum/pay_blacklist_type.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ja-JP/enum/pay_blacklist_type.json index a8e24b04a..9694d3917 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ja-JP/enum/pay_blacklist_type.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ja-JP/enum/pay_blacklist_type.json @@ -1,4 +1,5 @@ { "ip": "IP", - "open_id": "OpenId" + "alipay_user": "Alipay ユーザー", + "wechat_openid": "WeChat OpenId" } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ja-JP/pay/error.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ja-JP/pay/error.json index 019574aa4..1e20f9b2c 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ja-JP/pay/error.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ja-JP/pay/error.json @@ -132,6 +132,8 @@ "blacklistNotFound": "ブラックリストが存在しません", "blacklistTypeInvalid": "ブラックリスト種別が無効です", "blacklistStatusInvalid": "ブラックリスト状態が無効です", + "blacklistWechatAppRequired": "WeChat OpenId ブラックリストには決済アプリの指定が必要です", + "blacklistChannelInvalid": "ブラックリストのチャネルが無効です", "hitNotFound": "リスクヒット記録が存在しません", "handleStatusInvalid": "処理状態が無効です" } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ko-KR/enum/pay_blacklist_type.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ko-KR/enum/pay_blacklist_type.json index a8e24b04a..d7716e9c3 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ko-KR/enum/pay_blacklist_type.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ko-KR/enum/pay_blacklist_type.json @@ -1,4 +1,5 @@ { "ip": "IP", - "open_id": "OpenId" + "alipay_user": "알리페이 사용자", + "wechat_openid": "위챗 OpenId" } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ko-KR/pay/error.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ko-KR/pay/error.json index 7f7f5c563..8d4a20eba 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ko-KR/pay/error.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ko-KR/pay/error.json @@ -132,6 +132,8 @@ "blacklistNotFound": "블랙리스트가 존재하지 않습니다", "blacklistTypeInvalid": "블랙리스트 유형이 유효하지 않습니다", "blacklistStatusInvalid": "블랙리스트 상태가 유효하지 않습니다", + "blacklistWechatAppRequired": "위챗 OpenId 블랙리스트에는 결제 앱을 지정해야 합니다", + "blacklistChannelInvalid": "블랙리스트 채널이 유효하지 않습니다", "hitNotFound": "리스크 적중 기록이 존재하지 않습니다", "handleStatusInvalid": "처리 상태가 유효하지 않습니다" } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ms-MY/enum/pay_blacklist_type.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ms-MY/enum/pay_blacklist_type.json index a8e24b04a..ad4167299 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ms-MY/enum/pay_blacklist_type.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ms-MY/enum/pay_blacklist_type.json @@ -1,4 +1,5 @@ { "ip": "IP", - "open_id": "OpenId" + "alipay_user": "Pengguna Alipay", + "wechat_openid": "OpenId WeChat" } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ms-MY/pay/error.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ms-MY/pay/error.json index 99671feab..e5782b97d 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ms-MY/pay/error.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ms-MY/pay/error.json @@ -131,6 +131,8 @@ "blacklistNotFound": "Entri senarai hitam tidak ditemui", "blacklistTypeInvalid": "Jenis senarai hitam tidak sah", "blacklistStatusInvalid": "Status senarai hitam tidak sah", + "blacklistWechatAppRequired": "Senarai hitam OpenId WeChat mesti menetapkan aplikasi pembayaran", + "blacklistChannelInvalid": "Saluran senarai hitam tidak sah", "hitNotFound": "Rekod hit risiko tidak ditemui", "handleStatusInvalid": "Status pengendalian tidak sah" } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/th-TH/enum/pay_blacklist_type.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/th-TH/enum/pay_blacklist_type.json index a8e24b04a..8bef83a47 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/th-TH/enum/pay_blacklist_type.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/th-TH/enum/pay_blacklist_type.json @@ -1,4 +1,5 @@ { "ip": "IP", - "open_id": "OpenId" + "alipay_user": "ผู้ใช้ Alipay", + "wechat_openid": "OpenId WeChat" } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/th-TH/pay/error.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/th-TH/pay/error.json index 003aa0556..6b9f0e630 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/th-TH/pay/error.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/th-TH/pay/error.json @@ -131,6 +131,8 @@ "blacklistNotFound": "ไม่พบรายการบัญชีดำ", "blacklistTypeInvalid": "ประเภทบัญชีดำไม่ถูกต้อง", "blacklistStatusInvalid": "สถานะบัญชีดำไม่ถูกต้อง", + "blacklistWechatAppRequired": "บัญชีดำ OpenId WeChat ต้องระบุแอปชำระเงิน", + "blacklistChannelInvalid": "ช่องทางบัญชีดำไม่ถูกต้อง", "hitNotFound": "ไม่พบบันทึกการตรวจพบความเสี่ยง", "handleStatusInvalid": "สถานะการจัดการไม่ถูกต้อง" } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/vi-VN/enum/pay_blacklist_type.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/vi-VN/enum/pay_blacklist_type.json index a8e24b04a..4959ef2c6 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/vi-VN/enum/pay_blacklist_type.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/vi-VN/enum/pay_blacklist_type.json @@ -1,4 +1,5 @@ { "ip": "IP", - "open_id": "OpenId" + "alipay_user": "Người dùng Alipay", + "wechat_openid": "OpenId WeChat" } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/vi-VN/pay/error.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/vi-VN/pay/error.json index d2ecf1ab0..1aac51f1b 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/vi-VN/pay/error.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/vi-VN/pay/error.json @@ -131,6 +131,8 @@ "blacklistNotFound": "Không tìm thấy mục danh sách đen", "blacklistTypeInvalid": "Loại danh sách đen không hợp lệ", "blacklistStatusInvalid": "Trạng thái danh sách đen không hợp lệ", + "blacklistWechatAppRequired": "Danh sách đen OpenId WeChat phải chọn ứng dụng thanh toán", + "blacklistChannelInvalid": "Kênh danh sách đen không hợp lệ", "hitNotFound": "Không tìm thấy bản ghi trúng rủi ro", "handleStatusInvalid": "Trạng thái xử lý không hợp lệ" } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/enum/pay_blacklist_type.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/enum/pay_blacklist_type.json index a8e24b04a..38e0a115d 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/enum/pay_blacklist_type.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/enum/pay_blacklist_type.json @@ -1,4 +1,5 @@ { "ip": "IP", - "open_id": "OpenId" + "alipay_user": "支付宝用户", + "wechat_openid": "微信 OpenId" } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/pay/error.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/pay/error.json index e71751941..7f9211da6 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/pay/error.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/pay/error.json @@ -132,6 +132,8 @@ "blacklistNotFound": "黑名单不存在", "blacklistTypeInvalid": "黑名单类型无效", "blacklistStatusInvalid": "黑名单状态无效", + "blacklistWechatAppRequired": "微信 OpenId 黑名单必须指定支付应用", + "blacklistChannelInvalid": "黑名单通道无效", "hitNotFound": "风险命中记录不存在", "handleStatusInvalid": "处理状态无效" } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-HK/enum/pay_blacklist_type.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-HK/enum/pay_blacklist_type.json index a8e24b04a..d01e26988 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-HK/enum/pay_blacklist_type.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-HK/enum/pay_blacklist_type.json @@ -1,4 +1,5 @@ { "ip": "IP", - "open_id": "OpenId" + "alipay_user": "支付寶用戶", + "wechat_openid": "微信 OpenId" } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-HK/pay/error.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-HK/pay/error.json index be8538255..99dbdf7d2 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-HK/pay/error.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-HK/pay/error.json @@ -132,6 +132,8 @@ "blacklistNotFound": "黑名單不存在", "blacklistTypeInvalid": "黑名單類型無效", "blacklistStatusInvalid": "黑名單狀態無效", + "blacklistWechatAppRequired": "微信 OpenId 黑名單必須指定支付應用", + "blacklistChannelInvalid": "黑名單通道無效", "hitNotFound": "風險命中記錄不存在", "handleStatusInvalid": "處理狀態無效" } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-TW/enum/pay_blacklist_type.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-TW/enum/pay_blacklist_type.json index a8e24b04a..d01e26988 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-TW/enum/pay_blacklist_type.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-TW/enum/pay_blacklist_type.json @@ -1,4 +1,5 @@ { "ip": "IP", - "open_id": "OpenId" + "alipay_user": "支付寶用戶", + "wechat_openid": "微信 OpenId" } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-TW/pay/error.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-TW/pay/error.json index 3fe8171a8..423d8487e 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-TW/pay/error.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-TW/pay/error.json @@ -132,6 +132,8 @@ "blacklistNotFound": "黑名單不存在", "blacklistTypeInvalid": "黑名單類型無效", "blacklistStatusInvalid": "黑名單狀態無效", + "blacklistWechatAppRequired": "微信 OpenId 黑名單必須指定支付應用", + "blacklistChannelInvalid": "黑名單通道無效", "hitNotFound": "風險命中記錄不存在", "handleStatusInvalid": "處理狀態無效" } diff --git a/daxpay-plugin/daxpay-plugin-risk/src/main/java/cn/daxpay/open/plugin/risk/dao/PayBlacklistManager.java b/daxpay-plugin/daxpay-plugin-risk/src/main/java/cn/daxpay/open/plugin/risk/dao/PayBlacklistManager.java index 40fafbd9d..ac10adeac 100644 --- a/daxpay-plugin/daxpay-plugin-risk/src/main/java/cn/daxpay/open/plugin/risk/dao/PayBlacklistManager.java +++ b/daxpay-plugin/daxpay-plugin-risk/src/main/java/cn/daxpay/open/plugin/risk/dao/PayBlacklistManager.java @@ -15,7 +15,6 @@ import org.springframework.stereotype.Repository; import java.time.OffsetDateTime; import java.time.ZoneOffset; -import java.util.List; import java.util.Optional; /// # 黑名单 Manager @@ -31,25 +30,17 @@ public class PayBlacklistManager extends BaseManager { - if (StrUtil.isBlank(ch)) { - w.and(x -> x.isNull(PayBlacklist::getChannel).or().eq(PayBlacklist::getChannel, "")); - } else { - w.eq(PayBlacklist::getChannel, ch); - } - }) .and(w -> { if (StrUtil.isBlank(app)) { - w.and(x -> x.isNull(PayBlacklist::getChannelAppId).or().eq(PayBlacklist::getChannelAppId, "")); + w.and(x -> x.isNull(PayBlacklist::getWxAppId).or().eq(PayBlacklist::getWxAppId, "")); } else { - w.eq(PayBlacklist::getChannelAppId, app); + w.eq(PayBlacklist::getWxAppId, app); } }) .ne(excludeId != null, PayBlacklist::getId, excludeId) @@ -57,47 +48,40 @@ public class PayBlacklistManager extends BaseManager findActiveHit(String type, String value, String channel, String channelAppId) { + /// + /// - ip / alipay_user:按 type+value,忽略 wxAppId + /// - wechat_openid:按 type+value+wxAppId 精确匹配(wxAppId 为空则不命中) + public Optional findActiveHit(String type, String value, String wxAppId) { if (StrUtil.isBlank(type) || StrUtil.isBlank(value)) { return Optional.empty(); } OffsetDateTime now = OffsetDateTime.now(ZoneOffset.UTC); - List list = lambdaQuery() + var query = lambdaQuery() .eq(PayBlacklist::getType, type) .eq(PayBlacklist::getValue, value) .eq(PayBlacklist::getStatus, PayBlacklistStatusEnum.ENABLE.getCode()) - .and(w -> w.isNull(PayBlacklist::getExpireTime).or().gt(PayBlacklist::getExpireTime, now)) - .list(); - if (list.isEmpty()) { - return Optional.empty(); + .and(w -> w.isNull(PayBlacklist::getExpireTime).or().gt(PayBlacklist::getExpireTime, now)); + if (PayBlacklistTypeEnum.WECHAT_OPENID.getCode().equals(type)) { + if (StrUtil.isBlank(wxAppId)) { + return Optional.empty(); + } + query.eq(PayBlacklist::getWxAppId, wxAppId); + } else { + // ip / alipay_user:wx_app_id 必须为空 + query.and(w -> w.isNull(PayBlacklist::getWxAppId).or().eq(PayBlacklist::getWxAppId, "")); } - // 优先精确 channel + channelAppId;再宽匹配 channel 空 - Optional exact = list.stream() - .filter(e -> matchNullable(e.getChannel(), channel) && matchNullable(e.getChannelAppId(), channelAppId)) - .findFirst(); - if (exact.isPresent()) { - return exact; - } - // 名单 channel 为空视为对该 value 全局生效 - return list.stream() - .filter(e -> StrUtil.isBlank(e.getChannel()) && StrUtil.isBlank(e.getChannelAppId())) - .findFirst(); + return query.oneOpt(); } - /// 是否存在有效的 openId 类型黑名单(enable 且未过期) - public boolean hasActiveOpenIdBlacklist() { + /// 是否存在有效的用户标识类黑名单(支付宝 / 微信) + public boolean hasActiveUserIdentityBlacklist() { OffsetDateTime now = OffsetDateTime.now(ZoneOffset.UTC); return lambdaQuery() - .eq(PayBlacklist::getType, PayBlacklistTypeEnum.OPEN_ID.getCode()) + .in(PayBlacklist::getType, + PayBlacklistTypeEnum.ALIPAY_USER.getCode(), + PayBlacklistTypeEnum.WECHAT_OPENID.getCode()) .eq(PayBlacklist::getStatus, PayBlacklistStatusEnum.ENABLE.getCode()) .and(w -> w.isNull(PayBlacklist::getExpireTime).or().gt(PayBlacklist::getExpireTime, now)) .exists(); } - - private static boolean matchNullable(String rule, String actual) { - if (StrUtil.isBlank(rule)) { - return StrUtil.isBlank(actual); - } - return rule.equals(actual); - } } diff --git a/daxpay-plugin/daxpay-plugin-risk/src/main/java/cn/daxpay/open/plugin/risk/entity/PayBlacklist.java b/daxpay-plugin/daxpay-plugin-risk/src/main/java/cn/daxpay/open/plugin/risk/entity/PayBlacklist.java index 214885b7f..c76df1e8e 100644 --- a/daxpay-plugin/daxpay-plugin-risk/src/main/java/cn/daxpay/open/plugin/risk/entity/PayBlacklist.java +++ b/daxpay-plugin/daxpay-plugin-risk/src/main/java/cn/daxpay/open/plugin/risk/entity/PayBlacklist.java @@ -16,7 +16,10 @@ import java.time.OffsetDateTime; /// # 支付黑名单配置 /// -/// 平台级名单,维度为 IP / openId,不含商户号事前拉黑。 +/// 平台级名单,不含商户号事前拉黑。 +/// - [PayBlacklistTypeEnum#IP]:全局 IP +/// - [PayBlacklistTypeEnum#ALIPAY_USER]:支付宝 userId(不区分应用) +/// - [PayBlacklistTypeEnum#WECHAT_OPENID]:微信 openId,必须绑定 [wxAppId] @EqualsAndHashCode(callSuper = true) @Data @FieldNameConstants @@ -28,14 +31,11 @@ public class PayBlacklist extends MpBaseEntity implements ToResult findByCode(String code) { return Arrays.stream(values()).filter(e -> e.code.equals(code)).findFirst(); } + + /// 是否为用户标识类名单(支付宝 / 微信) + public boolean isUserIdentity() { + return this == ALIPAY_USER || this == WECHAT_OPENID; + } } diff --git a/daxpay-plugin/daxpay-plugin-risk/src/main/java/cn/daxpay/open/plugin/risk/param/PayBlacklistParam.java b/daxpay-plugin/daxpay-plugin-risk/src/main/java/cn/daxpay/open/plugin/risk/param/PayBlacklistParam.java index 86c6b9c1f..61db57e9d 100644 --- a/daxpay-plugin/daxpay-plugin-risk/src/main/java/cn/daxpay/open/plugin/risk/param/PayBlacklistParam.java +++ b/daxpay-plugin/daxpay-plugin-risk/src/main/java/cn/daxpay/open/plugin/risk/param/PayBlacklistParam.java @@ -22,7 +22,7 @@ public class PayBlacklistParam { @NotNull(message = "{validation.field.id.notNull}", groups = ValidationGroup.edit.class) private Long id; - /// 类型:ip / open_id + /// 类型:ip / alipay_user / wechat_openid @Schema(description = "类型") @NotBlank(message = "{validation.field.type.notBlank}", groups = ValidationGroup.add.class) @Size(max = 32, message = "{validation.field.product.size}") @@ -34,13 +34,10 @@ public class PayBlacklistParam { @Size(max = 128, message = "{validation.field.openId.size}") private String value; - @Schema(description = "通道族") - @Size(max = 32, message = "{validation.field.channel.size}") - private String channel; - - @Schema(description = "通道应用AppId") + /// 微信平台支付应用 AppId(仅 wechat_openid 必填) + @Schema(description = "微信平台支付应用 AppId") @Size(max = 64, message = "{validation.field.channelAppId.size}") - private String channelAppId; + private String wxAppId; /// 状态:enable / disable @Schema(description = "状态") diff --git a/daxpay-plugin/daxpay-plugin-risk/src/main/java/cn/daxpay/open/plugin/risk/param/PayBlacklistQuery.java b/daxpay-plugin/daxpay-plugin-risk/src/main/java/cn/daxpay/open/plugin/risk/param/PayBlacklistQuery.java index aabcf06d8..2cc5d0e20 100644 --- a/daxpay-plugin/daxpay-plugin-risk/src/main/java/cn/daxpay/open/plugin/risk/param/PayBlacklistQuery.java +++ b/daxpay-plugin/daxpay-plugin-risk/src/main/java/cn/daxpay/open/plugin/risk/param/PayBlacklistQuery.java @@ -23,6 +23,6 @@ public class PayBlacklistQuery { @Schema(description = "状态") private String status; - @Schema(description = "通道族") - private String channel; + @Schema(description = "微信平台支付应用 AppId") + private String wxAppId; } diff --git a/daxpay-plugin/daxpay-plugin-risk/src/main/java/cn/daxpay/open/plugin/risk/result/PayBlacklistResult.java b/daxpay-plugin/daxpay-plugin-risk/src/main/java/cn/daxpay/open/plugin/risk/result/PayBlacklistResult.java index 375c5dc35..3ac1d48c2 100644 --- a/daxpay-plugin/daxpay-plugin-risk/src/main/java/cn/daxpay/open/plugin/risk/result/PayBlacklistResult.java +++ b/daxpay-plugin/daxpay-plugin-risk/src/main/java/cn/daxpay/open/plugin/risk/result/PayBlacklistResult.java @@ -22,11 +22,8 @@ public class PayBlacklistResult extends BaseResult { @Schema(description = "名单值") private String value; - @Schema(description = "通道族") - private String channel; - - @Schema(description = "通道应用AppId") - private String channelAppId; + @Schema(description = "微信平台支付应用 AppId") + private String wxAppId; @Schema(description = "状态") private String status; diff --git a/daxpay-plugin/daxpay-plugin-risk/src/main/java/cn/daxpay/open/plugin/risk/service/PayBlacklistService.java b/daxpay-plugin/daxpay-plugin-risk/src/main/java/cn/daxpay/open/plugin/risk/service/PayBlacklistService.java index eff20ef74..142b00e33 100644 --- a/daxpay-plugin/daxpay-plugin-risk/src/main/java/cn/daxpay/open/plugin/risk/service/PayBlacklistService.java +++ b/daxpay-plugin/daxpay-plugin-risk/src/main/java/cn/daxpay/open/plugin/risk/service/PayBlacklistService.java @@ -2,6 +2,7 @@ package cn.daxpay.open.plugin.risk.service; import cn.daxpay.open.platform.common.mybatisplus.util.MpUtil; import cn.daxpay.open.platform.core.code.PayErrorCode; +import cn.daxpay.open.platform.core.enums.pay.channel.ChannelEnum; import cn.daxpay.open.platform.core.exception.BizInfoException; import cn.daxpay.open.platform.core.exception.DataNotExistException; import cn.daxpay.open.platform.core.rest.param.PageParam; @@ -48,8 +49,8 @@ public class PayBlacklistService { if (StrUtil.isNotBlank(param.getStatus())) { validateStatus(param.getStatus()); } - if (payBlacklistManager.existsDuplicate(param.getType(), param.getValue(), - param.getChannel(), param.getChannelAppId(), null)) { + normalizeScope(param); + if (payBlacklistManager.existsDuplicate(param.getType(), param.getValue(), param.getWxAppId(), null)) { // 黑名单已存在 throw new BizInfoException(PayErrorCode.OPERATION_FAIL, "pay.error.risk.blacklistDuplicate"); } @@ -67,14 +68,22 @@ public class PayBlacklistService { PayBlacklist entity = getEntity(param.getId()); String originType = entity.getType(); String originValue = entity.getValue(); + param.setType(originType); + normalizeScope(param); PayBlacklistConvert.CONVERT.copy(param, entity); entity.setType(originType); entity.setValue(originValue); + // MapStruct IGNORE 不会清空 null + if (!PayBlacklistTypeEnum.WECHAT_OPENID.getCode().equals(originType)) { + entity.setWxAppId(null); + } else { + entity.setWxAppId(StrUtil.trimToNull(param.getWxAppId())); + } if (StrUtil.isNotBlank(param.getStatus())) { validateStatus(param.getStatus()); } if (payBlacklistManager.existsDuplicate(entity.getType(), entity.getValue(), - entity.getChannel(), entity.getChannelAppId(), entity.getId())) { + entity.getWxAppId(), entity.getId())) { throw new BizInfoException(PayErrorCode.OPERATION_FAIL, "pay.error.risk.blacklistDuplicate"); } payBlacklistManager.updateById(entity); @@ -88,37 +97,107 @@ public class PayBlacklistService { } /// 是否命中有效黑名单 - public boolean isBlocked(String type, String value, String channel, String channelAppId) { - return findActive(type, value, channel, channelAppId).isPresent(); + public boolean isBlocked(String type, String value, String wxAppId) { + return findActive(type, value, wxAppId).isPresent(); } - /// 是否存在有效的 openId 类型黑名单(供网关层智能触发 OAuth) + /// 是否存在有效的用户标识类黑名单(供网关层智能触发 OAuth) public boolean hasActiveOpenIdBlacklist() { - return payBlacklistManager.hasActiveOpenIdBlacklist(); + return payBlacklistManager.hasActiveUserIdentityBlacklist(); } /// 查找有效名单行 - public Optional findActive(String type, String value, String channel, String channelAppId) { - return payBlacklistManager.findActiveHit(type, value, channel, channelAppId); + public Optional findActive(String type, String value, String wxAppId) { + return payBlacklistManager.findActiveHit(type, value, wxAppId); } - /// 供命中处理「加入黑名单」:无有效项则新建 + /// 供命中处理「加入黑名单」 + /// + /// 按 hit 的 type/channel 映射;微信无 wxAppId 时抛错(避免非法行)。 @Transactional(rollbackFor = Exception.class) - public PayBlacklist ensureBlacklist(String type, String value, String channel, String reason) { - Optional existing = payBlacklistManager.findActiveHit(type, value, channel, null); + public PayBlacklist ensureBlacklist(String hitType, String value, String channel, + String wxAppId, String reason) { + if (StrUtil.isBlank(value)) { + throw new BizInfoException(PayErrorCode.OPERATION_FAIL, "pay.error.risk.blacklistTypeInvalid"); + } + ResolvedIdentity resolved = resolveIdentity(hitType, channel, wxAppId); + if (resolved == null) { + // 微信缺 AppId 或无法映射 + if (ChannelEnum.WECHAT.getCode().equals(channel) + || PayBlacklistTypeEnum.WECHAT_OPENID.getCode().equals(hitType) + || ("open_id".equals(hitType) && ChannelEnum.WECHAT.getCode().equals(channel))) { + throw new BizInfoException(PayErrorCode.OPERATION_FAIL, "pay.error.risk.blacklistWechatAppRequired"); + } + throw new BizInfoException(PayErrorCode.OPERATION_FAIL, "pay.error.risk.blacklistChannelInvalid"); + } + Optional existing = payBlacklistManager.findActiveHit( + resolved.type(), value, resolved.wxAppId()); if (existing.isPresent()) { return existing.get(); } PayBlacklist entity = new PayBlacklist() - .setType(type) + .setType(resolved.type()) .setValue(value) - .setChannel(channel) + .setWxAppId(resolved.wxAppId()) .setStatus(PayBlacklistStatusEnum.ENABLE.getCode()) .setReason(StrUtil.blankToDefault(reason, "risk hit auto add")); payBlacklistManager.save(entity); return entity; } + /// 规范化作用域:微信必须 wxAppId;其它类型强制清空 + private void normalizeScope(PayBlacklistParam param) { + String type = param.getType(); + if (PayBlacklistTypeEnum.WECHAT_OPENID.getCode().equals(type)) { + String wxAppId = StrUtil.trimToNull(param.getWxAppId()); + if (StrUtil.isBlank(wxAppId)) { + throw new BizInfoException(PayErrorCode.OPERATION_FAIL, "pay.error.risk.blacklistWechatAppRequired"); + } + param.setWxAppId(wxAppId); + return; + } + if (PayBlacklistTypeEnum.IP.getCode().equals(type) + || PayBlacklistTypeEnum.ALIPAY_USER.getCode().equals(type)) { + param.setWxAppId(null); + return; + } + throw new BizInfoException(PayErrorCode.OPERATION_FAIL, "pay.error.risk.blacklistTypeInvalid"); + } + + /// 将命中快照映射为名单 type + wxAppId;微信缺 AppId 则返回 null + private ResolvedIdentity resolveIdentity(String hitType, String channel, String wxAppId) { + if (PayBlacklistTypeEnum.IP.getCode().equals(hitType)) { + return new ResolvedIdentity(PayBlacklistTypeEnum.IP.getCode(), null); + } + if (PayBlacklistTypeEnum.ALIPAY_USER.getCode().equals(hitType) + || ChannelEnum.ALIPAY.getCode().equals(channel)) { + return new ResolvedIdentity(PayBlacklistTypeEnum.ALIPAY_USER.getCode(), null); + } + if (PayBlacklistTypeEnum.WECHAT_OPENID.getCode().equals(hitType) + || ChannelEnum.WECHAT.getCode().equals(channel)) { + String app = StrUtil.trimToNull(wxAppId); + if (StrUtil.isBlank(app)) { + log.warn("命中加黑跳过:微信名单缺少 wxAppId, hitType={}, channel={}", hitType, channel); + return null; + } + return new ResolvedIdentity(PayBlacklistTypeEnum.WECHAT_OPENID.getCode(), app); + } + // 兼容旧 hitType=open_id + if ("open_id".equals(hitType)) { + if (ChannelEnum.ALIPAY.getCode().equals(channel)) { + return new ResolvedIdentity(PayBlacklistTypeEnum.ALIPAY_USER.getCode(), null); + } + if (ChannelEnum.WECHAT.getCode().equals(channel)) { + String app = StrUtil.trimToNull(wxAppId); + if (StrUtil.isBlank(app)) { + return null; + } + return new ResolvedIdentity(PayBlacklistTypeEnum.WECHAT_OPENID.getCode(), app); + } + } + return null; + } + private PayBlacklist getEntity(Long id) { return payBlacklistManager.findById(id) // 黑名单不存在 @@ -136,4 +215,7 @@ public class PayBlacklistService { throw new BizInfoException(PayErrorCode.OPERATION_FAIL, "pay.error.risk.blacklistStatusInvalid"); } } + + private record ResolvedIdentity(String type, String wxAppId) { + } } diff --git a/daxpay-plugin/daxpay-plugin-risk/src/main/java/cn/daxpay/open/plugin/risk/service/PayRiskHitService.java b/daxpay-plugin/daxpay-plugin-risk/src/main/java/cn/daxpay/open/plugin/risk/service/PayRiskHitService.java index 7368f2032..f407d20ce 100644 --- a/daxpay-plugin/daxpay-plugin-risk/src/main/java/cn/daxpay/open/plugin/risk/service/PayRiskHitService.java +++ b/daxpay-plugin/daxpay-plugin-risk/src/main/java/cn/daxpay/open/plugin/risk/service/PayRiskHitService.java @@ -62,10 +62,12 @@ public class PayRiskHitService { "pay.error.risk.handleStatusInvalid")); PayRiskHit entity = getEntity(param.getId()); if (status == PayRiskHitHandleStatusEnum.ADDED_BLACKLIST) { + // 微信名单需 wxAppId;命中快照无该字段时无法自动写入 PayBlacklist bl = payBlacklistService.ensureBlacklist( entity.getHitType(), entity.getHitValue(), entity.getChannel(), + null, param.getHandleRemark()); entity.setBlacklistId(bl.getId()); } diff --git a/daxpay-plugin/daxpay-plugin-risk/src/main/java/cn/daxpay/open/plugin/risk/strategy/DefaultPayRiskChecker.java b/daxpay-plugin/daxpay-plugin-risk/src/main/java/cn/daxpay/open/plugin/risk/strategy/DefaultPayRiskChecker.java index ee23c02b5..85d80aeb2 100644 --- a/daxpay-plugin/daxpay-plugin-risk/src/main/java/cn/daxpay/open/plugin/risk/strategy/DefaultPayRiskChecker.java +++ b/daxpay-plugin/daxpay-plugin-risk/src/main/java/cn/daxpay/open/plugin/risk/strategy/DefaultPayRiskChecker.java @@ -3,6 +3,7 @@ package cn.daxpay.open.plugin.risk.strategy; import cn.daxpay.open.payment.strategy.risk.PayRiskCheckContext; import cn.daxpay.open.payment.strategy.risk.PayRiskChecker; import cn.daxpay.open.platform.core.code.PayErrorCode; +import cn.daxpay.open.platform.core.enums.pay.channel.ChannelEnum; import cn.daxpay.open.platform.core.exception.BizInfoException; import cn.daxpay.open.plugin.risk.entity.PayBlacklist; import cn.daxpay.open.plugin.risk.enums.PayBlacklistTypeEnum; @@ -31,7 +32,7 @@ public class DefaultPayRiskChecker implements PayRiskChecker { private final PayBlacklistService payBlacklistService; private final PayRiskHitService payRiskHitService; - /// openId 黑名单存在性缓存(短 TTL 30s) + /// 用户标识黑名单存在性缓存(短 TTL 30s) /// /// 仅供网关层判断是否触发强制 OAuth 取 openId, 非关键路径, /// 30s 延迟可接受(黑名单 CRUD 不会立刻反映到 OAuth 触发判定) @@ -49,12 +50,11 @@ public class DefaultPayRiskChecker implements PayRiskChecker { } ctx.setPhase(PayRiskHitPhaseEnum.BEFORE_PAY.getCode()); // IP 名单(全局生效) - rejectIfBlocked(ctx, PayBlacklistTypeEnum.IP.getCode(), ctx.getClientIp(), null, null, true); - // openId 名单(按通道精细匹配) - boolean openIdBlocked = rejectIfBlocked(ctx, PayBlacklistTypeEnum.OPEN_ID.getCode(), ctx.getOpenId(), - ctx.getChannel(), ctx.getChannelAppId(), true); - if (!openIdBlocked && StrUtil.isBlank(ctx.getOpenId())) { - log.warn("支付前 openId 缺失, openId 黑名单降级为仅 IP 校验 + 事后补录: " + rejectIfBlocked(ctx, PayBlacklistTypeEnum.IP.getCode(), ctx.getClientIp(), null, true); + // 用户标识:按通道映射名单类型 + boolean identityBlocked = checkUserIdentity(ctx, ctx.getOpenId(), true); + if (!identityBlocked && StrUtil.isBlank(ctx.getOpenId())) { + log.warn("支付前 openId 缺失, 用户标识黑名单降级为仅 IP 校验 + 事后补录: " + "tradeType={}, method={}, mchNo={}, clientIp={}", ctx.getTradeType(), ctx.getMethod(), ctx.getMchNo(), ctx.getClientIp()); } @@ -67,13 +67,11 @@ public class DefaultPayRiskChecker implements PayRiskChecker { } ctx.setPhase(PayRiskHitPhaseEnum.AFTER_PAY.getCode()); // 事后只记命中,不抛错 - rejectIfBlocked(ctx, PayBlacklistTypeEnum.IP.getCode(), ctx.getClientIp(), null, null, false); - rejectIfBlocked(ctx, PayBlacklistTypeEnum.OPEN_ID.getCode(), ctx.getOpenId(), - ctx.getChannel(), ctx.getChannelAppId(), false); - // buyerId 按 open_id 维度比对(主扫补洞) + rejectIfBlocked(ctx, PayBlacklistTypeEnum.IP.getCode(), ctx.getClientIp(), null, false); + checkUserIdentity(ctx, ctx.getOpenId(), false); + // buyerId 按用户标识维度比对(主扫补洞) if (StrUtil.isNotBlank(ctx.getBuyerId()) && !StrUtil.equals(ctx.getBuyerId(), ctx.getOpenId())) { - rejectIfBlocked(ctx, PayBlacklistTypeEnum.OPEN_ID.getCode(), ctx.getBuyerId(), - ctx.getChannel(), ctx.getChannelAppId(), false); + checkUserIdentity(ctx, ctx.getBuyerId(), false); } } @@ -88,13 +86,30 @@ public class DefaultPayRiskChecker implements PayRiskChecker { return exists; } + /// 按请求通道检查支付宝 / 微信用户标识名单 + private boolean checkUserIdentity(PayRiskCheckContext ctx, String identity, boolean throwOnHit) { + if (StrUtil.isBlank(identity)) { + return false; + } + String channel = ctx.getChannel(); + if (ChannelEnum.ALIPAY.getCode().equals(channel)) { + return rejectIfBlocked(ctx, PayBlacklistTypeEnum.ALIPAY_USER.getCode(), identity, null, throwOnHit); + } + if (ChannelEnum.WECHAT.getCode().equals(channel)) { + return rejectIfBlocked(ctx, PayBlacklistTypeEnum.WECHAT_OPENID.getCode(), identity, + ctx.getChannelAppId(), throwOnHit); + } + // 其它通道本期不查用户标识名单 + return false; + } + /// 返回是否命中(用于外层判断是否需要打降级日志) private boolean rejectIfBlocked(PayRiskCheckContext ctx, String type, String value, - String channel, String channelAppId, boolean throwOnHit) { + String wxAppId, boolean throwOnHit) { if (StrUtil.isBlank(value)) { return false; } - Optional hit = payBlacklistService.findActive(type, value, channel, channelAppId); + Optional hit = payBlacklistService.findActive(type, value, wxAppId); if (hit.isEmpty()) { return false; }