From bc4e388dfd32f006d03334bbec0ff1ad00e8aafd Mon Sep 17 00:00:00 2001 From: DaxPay Dev Date: Sat, 18 Jul 2026 23:56:22 +0800 Subject: [PATCH] =?UTF-8?q?feat(merchant):=20=E9=BB=98=E8=AE=A4=E5=BA=94?= =?UTF-8?q?=E7=94=A8/=E9=97=A8=E5=BA=97=E7=AD=96=E7=95=A5=E7=BB=9F?= =?UTF-8?q?=E4=B8=80=E6=B2=BB=E7=90=86=EF=BC=88=E5=94=AF=E4=B8=80=E7=BA=A6?= =?UTF-8?q?=E6=9D=9F/=E5=88=A0=E9=99=A4=E9=98=BB=E6=8C=A1/=E5=BC=82?= =?UTF-8?q?=E5=B8=B8=E5=88=86=E7=B1=BB=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 数据库加部分唯一索引 uk_mch_app_info_default / uk_mch_store_info_default 兜底 - Service.delete 检查默认项, 禁止删除默认应用/门店 - MchAppInfoService.add 首应用自动默认, 与 MchStoreInfoService 对齐 - resolveApp 异常分类: 默认应用不存在 vs 已停用 vs 指定应用异常 - resolveStoreNo 改抛异常 (替代返回 null), 与 resolveApp 策略对齐 - update-datas.sql 提供脏数据/缺默认项诊断查询 - 6 语种 i18n 新增 5 个错误 key --- _config/sql/update-datas.sql | 166 +++--- _config/sql/update-tables.sql | 487 +----------------- .../common/context/MerchantContextLoader.java | 15 +- .../dao/appinfo/MchAppInfoManager.java | 5 + .../merchant/entity/appinfo/MchAppInfo.java | 2 +- .../service/appinfo/MchAppInfoService.java | 18 +- .../service/store/MchStoreInfoService.java | 33 +- .../i18n/en-US/error/payment/merchant.json | 7 +- .../i18n/ja-JP/error/payment/merchant.json | 7 +- .../i18n/ko-KR/error/payment/merchant.json | 7 +- .../i18n/zh-CN/error/payment/merchant.json | 7 +- .../i18n/zh-HK/error/payment/merchant.json | 7 +- .../i18n/zh-TW/error/payment/merchant.json | 7 +- 13 files changed, 179 insertions(+), 589 deletions(-) diff --git a/_config/sql/update-datas.sql b/_config/sql/update-datas.sql index 3c2b64cb0..e974f8599 100644 --- a/_config/sql/update-datas.sql +++ b/_config/sql/update-datas.sql @@ -1,112 +1,62 @@ +-- ============================================================ +-- 门店/应用默认项数据迁移诊断(升级脚本) +-- ============================================================ +-- 背景: +-- 本次升级后, 支付下单若未显式传 appId / storeNo, +-- 系统会回落到商户的默认应用/默认门店; +-- 若默认项不存在或已停用, 将直接抛异常阻断下单(对齐 MchApp 既有策略)。 +-- 风险: +-- 历史数据中可能存在「从未创建默认应用/默认门店」的商户(如 SQL 直接导入未走入驻流程), +-- 升级后这些商户的下单会被阻断。 +-- 迁移策略: +-- 主键(id)与应用号(appId)/门店号(storeNo)由应用层(MyBatis-Plus 雪花 ID / RandomUtil)生成, +-- SQL 层难以精确复现; 且每个商户的默认应用名/门店名应按当前请求语言生成, +-- 不适合在 SQL 中硬编码。 +-- 因此本脚本不自动执行数据补建, 仅提供诊断查询, 由运维确认后处理。 +-- ============================================================ --- ---------------------------- --- 收银台模拟配置: 商户 M1781861282382 / 应用 A7846150576259390 --- H5(五环境 browser/wechat/alipay/union_pay/douyin) + WEB + MINI(四环境) --- resolve_mode 全部 method; 固定 ID 段 9100000000000100001~026, 可重复执行 --- ---------------------------- -DELETE FROM "public"."pay_gateway_cashier_item" -WHERE "mch_no" = 'M1781861282382' - AND "app_id" = 'A7846150576259390'; +-- 诊断 1: 查询没有默认应用的商户(升级后这些商户未传 appId 的下单会失败) +-- SELECT mch.mch_no, mch.mch_name +-- FROM merchant_info mch +-- WHERE NOT EXISTS ( +-- SELECT 1 FROM mch_app_info app +-- WHERE app.mch_no = mch.mch_no +-- AND app.default_app = TRUE +-- AND app.deleted = FALSE +-- ) +-- AND mch.deleted = FALSE; -INSERT INTO "public"."pay_gateway_cashier_item" ( - "id", "mch_no", "app_id", "cashier_type", "client_env", - "name", "icon", "recommend", "sort_no", - "resolve_mode", "method", "channel_mch_no", "capability", - "creator", "create_time", "last_modifier", "last_modified_time", - "version", "deleted" -) VALUES --- H5 · browser -(9100000000000100001, 'M1781861282382', 'A7846150576259390', 'h5', 'browser', - '微信支付', 'wechat', 't', 1, 'method', 'wechat_h5', NULL, NULL, - 1, '2026-07-17 00:00:00+00', 1, '2026-07-17 00:00:00+00', 0, 'f'), -(9100000000000100002, 'M1781861282382', 'A7846150576259390', 'h5', 'browser', - '支付宝', 'alipay', 'f', 2, 'method', 'alipay_h5', NULL, NULL, - 1, '2026-07-17 00:00:00+00', 1, '2026-07-17 00:00:00+00', 0, 'f'), -(9100000000000100003, 'M1781861282382', 'A7846150576259390', 'h5', 'browser', - '云闪付', 'union_pay', 'f', 3, 'method', 'union_h5', NULL, NULL, - 1, '2026-07-17 00:00:00+00', 1, '2026-07-17 00:00:00+00', 0, 'f'), -(9100000000000100004, 'M1781861282382', 'A7846150576259390', 'h5', 'browser', - '抖音支付', 'douyin', 'f', 4, 'method', 'douyin_h5', NULL, NULL, - 1, '2026-07-17 00:00:00+00', 1, '2026-07-17 00:00:00+00', 0, 'f'), --- H5 · wechat -(9100000000000100005, 'M1781861282382', 'A7846150576259390', 'h5', 'wechat', - '微信支付', 'wechat', 't', 1, 'method', 'wechat_jsapi', NULL, NULL, - 1, '2026-07-17 00:00:00+00', 1, '2026-07-17 00:00:00+00', 0, 'f'), -(9100000000000100006, 'M1781861282382', 'A7846150576259390', 'h5', 'wechat', - '微信扫码', 'wechat', 'f', 2, 'method', 'wechat_qr', NULL, NULL, - 1, '2026-07-17 00:00:00+00', 1, '2026-07-17 00:00:00+00', 0, 'f'), --- H5 · alipay -(9100000000000100007, 'M1781861282382', 'A7846150576259390', 'h5', 'alipay', - '支付宝', 'alipay', 't', 1, 'method', 'alipay_jsapi', NULL, NULL, - 1, '2026-07-17 00:00:00+00', 1, '2026-07-17 00:00:00+00', 0, 'f'), -(9100000000000100008, 'M1781861282382', 'A7846150576259390', 'h5', 'alipay', - '支付宝扫码', 'alipay', 'f', 2, 'method', 'alipay_qr', NULL, NULL, - 1, '2026-07-17 00:00:00+00', 1, '2026-07-17 00:00:00+00', 0, 'f'), --- H5 · union_pay -(9100000000000100009, 'M1781861282382', 'A7846150576259390', 'h5', 'union_pay', - '云闪付', 'union_pay', 't', 1, 'method', 'union_jsapi', NULL, NULL, - 1, '2026-07-17 00:00:00+00', 1, '2026-07-17 00:00:00+00', 0, 'f'), -(9100000000000100010, 'M1781861282382', 'A7846150576259390', 'h5', 'union_pay', - '银联扫码', 'union_pay', 'f', 2, 'method', 'union_qr', NULL, NULL, - 1, '2026-07-17 00:00:00+00', 1, '2026-07-17 00:00:00+00', 0, 'f'), --- H5 · douyin -(9100000000000100011, 'M1781861282382', 'A7846150576259390', 'h5', 'douyin', - '抖音支付', 'douyin', 't', 1, 'method', 'douyin_jsapi', NULL, NULL, - 1, '2026-07-17 00:00:00+00', 1, '2026-07-17 00:00:00+00', 0, 'f'), -(9100000000000100012, 'M1781861282382', 'A7846150576259390', 'h5', 'douyin', - '抖音扫码', 'douyin', 'f', 2, 'method', 'douyin_qr', NULL, NULL, - 1, '2026-07-17 00:00:00+00', 1, '2026-07-17 00:00:00+00', 0, 'f'), --- MINI · wechat -(9100000000000100013, 'M1781861282382', 'A7846150576259390', 'mini', 'wechat', - '微信支付', 'wechat', 't', 1, 'method', 'wechat_mini', NULL, NULL, - 1, '2026-07-17 00:00:00+00', 1, '2026-07-17 00:00:00+00', 0, 'f'), -(9100000000000100014, 'M1781861282382', 'A7846150576259390', 'mini', 'wechat', - '微信扫码', 'wechat', 'f', 2, 'method', 'wechat_qr', NULL, NULL, - 1, '2026-07-17 00:00:00+00', 1, '2026-07-17 00:00:00+00', 0, 'f'), --- MINI · alipay -(9100000000000100015, 'M1781861282382', 'A7846150576259390', 'mini', 'alipay', - '支付宝', 'alipay', 't', 1, 'method', 'alipay_jsapi', NULL, NULL, - 1, '2026-07-17 00:00:00+00', 1, '2026-07-17 00:00:00+00', 0, 'f'), -(9100000000000100016, 'M1781861282382', 'A7846150576259390', 'mini', 'alipay', - '支付宝扫码', 'alipay', 'f', 2, 'method', 'alipay_qr', NULL, NULL, - 1, '2026-07-17 00:00:00+00', 1, '2026-07-17 00:00:00+00', 0, 'f'), --- MINI · union_pay -(9100000000000100017, 'M1781861282382', 'A7846150576259390', 'mini', 'union_pay', - '云闪付', 'union_pay', 't', 1, 'method', 'union_jsapi', NULL, NULL, - 1, '2026-07-17 00:00:00+00', 1, '2026-07-17 00:00:00+00', 0, 'f'), -(9100000000000100018, 'M1781861282382', 'A7846150576259390', 'mini', 'union_pay', - '银联扫码', 'union_pay', 'f', 2, 'method', 'union_qr', NULL, NULL, - 1, '2026-07-17 00:00:00+00', 1, '2026-07-17 00:00:00+00', 0, 'f'), --- MINI · douyin -(9100000000000100019, 'M1781861282382', 'A7846150576259390', 'mini', 'douyin', - '抖音支付', 'douyin', 't', 1, 'method', 'douyin_jsapi', NULL, NULL, - 1, '2026-07-17 00:00:00+00', 1, '2026-07-17 00:00:00+00', 0, 'f'), -(9100000000000100020, 'M1781861282382', 'A7846150576259390', 'mini', 'douyin', - '抖音扫码', 'douyin', 'f', 2, 'method', 'douyin_qr', NULL, NULL, - 1, '2026-07-17 00:00:00+00', 1, '2026-07-17 00:00:00+00', 0, 'f'), --- WEB · 扁平(client_env 为空) -(9100000000000100021, 'M1781861282382', 'A7846150576259390', 'web', NULL, - '微信扫码', 'wechat', 't', 1, 'method', 'wechat_qr', NULL, NULL, - 1, '2026-07-17 00:00:00+00', 1, '2026-07-17 00:00:00+00', 0, 'f'), -(9100000000000100022, 'M1781861282382', 'A7846150576259390', 'web', NULL, - '支付宝扫码', 'alipay', 'f', 2, 'method', 'alipay_qr', NULL, NULL, - 1, '2026-07-17 00:00:00+00', 1, '2026-07-17 00:00:00+00', 0, 'f'), -(9100000000000100023, 'M1781861282382', 'A7846150576259390', 'web', NULL, - '支付宝电脑', 'alipay', 'f', 3, 'method', 'alipay_pc', NULL, NULL, - 1, '2026-07-17 00:00:00+00', 1, '2026-07-17 00:00:00+00', 0, 'f'), -(9100000000000100024, 'M1781861282382', 'A7846150576259390', 'web', NULL, - '银联扫码', 'union_pay', 'f', 4, 'method', 'union_qr', NULL, NULL, - 1, '2026-07-17 00:00:00+00', 1, '2026-07-17 00:00:00+00', 0, 'f'), -(9100000000000100025, 'M1781861282382', 'A7846150576259390', 'web', NULL, - '抖音扫码', 'douyin', 'f', 5, 'method', 'douyin_qr', NULL, NULL, - 1, '2026-07-17 00:00:00+00', 1, '2026-07-17 00:00:00+00', 0, 'f'), -(9100000000000100026, 'M1781861282382', 'A7846150576259390', 'web', NULL, - '微信 H5', 'wechat', 'f', 6, 'method', 'wechat_h5', NULL, NULL, - 1, '2026-07-17 00:00:00+00', 1, '2026-07-17 00:00:00+00', 0, 'f'); +-- 诊断 2: 查询没有默认门店的商户(升级后这些商户未传 storeNo 的下单会失败) +-- SELECT mch.mch_no, mch.mch_name +-- FROM merchant_info mch +-- WHERE NOT EXISTS ( +-- SELECT 1 FROM mch_store_info s +-- WHERE s.mch_no = mch.mch_no +-- AND s.default_store = TRUE +-- AND s.deleted = FALSE +-- ) +-- AND mch.deleted = FALSE; --- ---------------------------- --- 日志管理:支付接口审计菜单 system:log:unipay --- ---------------------------- -DELETE FROM "public"."iam_perm_menu" WHERE "id" = 30203; -INSERT INTO "public"."iam_perm_menu" VALUES (30203, 302, 'system:log:unipay', 'admin', 'SystemUnipayApiLog', 'menu.system.log.unipay', 'lucide:webhook', 'f', 'f', '/system/log/unipay/UnipayApiLogList', '/system/log/unipay', NULL, 3, 'f', 't', 'f', 0, 1, 1, 'f', 'menu', NULL, NULL, NULL, NULL, NULL, NULL, '2026-07-17 00:00:00+00', '2026-07-17 00:00:00+00'); +-- 诊断 3: 查询同商户存在多个默认应用的脏数据(会导致 uk_mch_app_info_default 索引创建失败) +-- SELECT mch_no, COUNT(*) +-- FROM mch_app_info +-- WHERE default_app = TRUE AND deleted = FALSE +-- GROUP BY mch_no +-- HAVING COUNT(*) > 1; +-- 诊断 4: 查询同商户存在多个默认门店的脏数据(会导致 uk_mch_store_info_default 索引创建失败) +-- SELECT mch_no, COUNT(*) +-- FROM mch_store_info +-- WHERE default_store = TRUE AND deleted = FALSE +-- GROUP BY mch_no +-- HAVING COUNT(*) > 1; + +-- ============================================================ +-- 修复指引(诊断查询有结果时按以下方式处理): +-- 1. 诊断 3/4 命中(脏数据): +-- 手动 UPDATE 保留其中一条 default_app/default_store = TRUE, 其余置 FALSE。 +-- 2. 诊断 1/2 命中(缺默认项): +-- 登录运营端 -> 商户管理 -> 选择对应商户 -> 应用管理/门店管理 -> +-- 新增应用/门店后调用「设为默认」接口(set-default); +-- 或通过 MerchantAdminService.createDefaultApp / MchStoreInfoService.createDefaultStore 触发。 +-- ============================================================ diff --git a/_config/sql/update-tables.sql b/_config/sql/update-tables.sql index f697989df..847529195 100644 --- a/_config/sql/update-tables.sql +++ b/_config/sql/update-tables.sql @@ -1,466 +1,25 @@ --- 支付风控:黑名单配置 + 风险命中记录(daxpay-plugin-risk) --- 升级脚本:已部署库执行本文件;COMMENT 使用 PG 标准 COMMENT ON +-- ============================================================ +-- 门店/应用默认项唯一性约束(升级脚本) +-- ============================================================ +-- 背景: +-- 商户应用(MchAppInfo)和门店(MchStoreInfo)均引入「默认项」机制, +-- 要求同一商户下至多一个默认应用 / 至多一个默认门店。 +-- 原有约束仅由 Service 层 setDefault(先 clearDefault 再 set)保证, +-- 并发或绕过 Service 直接改库时存在隐患。 +-- 策略: +-- 数据库层加 PostgreSQL 部分唯一索引(Partial Unique Index)兜底, +-- 与 Service 层逻辑形成双保险。 +-- 影响范围: +-- - 升级前请先执行 update-datas.sql 中的诊断查询, +-- 如发现「同商户多个默认应用/门店」需先人工修复, 否则建索引会失败。 +-- ============================================================ --- ---------------------------- --- Table structure for pay_blacklist --- ---------------------------- -CREATE TABLE IF NOT EXISTS "public"."pay_blacklist" ( - "id" int8 NOT NULL, - "type" varchar(32) COLLATE "pg_catalog"."default" NOT NULL, - "value" varchar(128) COLLATE "pg_catalog"."default" NOT NULL, - "channel" varchar(32) COLLATE "pg_catalog"."default", - "channel_app_id" varchar(64) COLLATE "pg_catalog"."default", - "status" varchar(16) COLLATE "pg_catalog"."default" NOT NULL, - "reason" varchar(255) COLLATE "pg_catalog"."default", - "expire_time" timestamptz(6), - "remark" varchar(255) COLLATE "pg_catalog"."default", - "creator" int8, - "create_time" timestamptz(6), - "last_modifier" int8, - "last_modified_time" timestamptz(6), - "version" int4 DEFAULT 0, - "deleted" bool NOT NULL DEFAULT false -); +-- 商户默认应用唯一索引 (同一商户至多一个 default_app=true 且未删除的记录) +CREATE UNIQUE INDEX IF NOT EXISTS uk_mch_app_info_default + ON mch_app_info (mch_no) + WHERE default_app = TRUE AND deleted = FALSE; -COMMENT ON TABLE "public"."pay_blacklist" IS '支付黑名单配置'; -COMMENT ON COLUMN "public"."pay_blacklist"."id" IS '主键'; -COMMENT ON COLUMN "public"."pay_blacklist"."type" IS '类型: ip/open_id'; -COMMENT ON COLUMN "public"."pay_blacklist"."value" IS '名单值(IP或openId)'; -COMMENT ON COLUMN "public"."pay_blacklist"."channel" IS '通道族(openId建议wechat/alipay, IP可空)'; -COMMENT ON COLUMN "public"."pay_blacklist"."channel_app_id" IS '通道应用AppId(可选)'; -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 '备注'; -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 '删除标志'; - -ALTER TABLE "public"."pay_blacklist" DROP CONSTRAINT IF EXISTS "pay_blacklist_pkey"; -ALTER TABLE "public"."pay_blacklist" ADD CONSTRAINT "pay_blacklist_pkey" PRIMARY KEY ("id"); - -CREATE INDEX IF NOT EXISTS "idx_pay_blacklist_type_value" ON "public"."pay_blacklist" ("type", "value"); -CREATE INDEX IF NOT EXISTS "idx_pay_blacklist_status_expire" ON "public"."pay_blacklist" ("status", "expire_time"); - --- ---------------------------- --- Table structure for pay_risk_hit --- ---------------------------- -CREATE TABLE IF NOT EXISTS "public"."pay_risk_hit" ( - "id" int8 NOT NULL, - "phase" varchar(16) COLLATE "pg_catalog"."default" NOT NULL, - "hit_type" varchar(32) COLLATE "pg_catalog"."default" NOT NULL, - "hit_value" varchar(128) COLLATE "pg_catalog"."default" NOT NULL, - "blacklist_id" int8, - "mch_no" varchar(32) COLLATE "pg_catalog"."default", - "app_id" varchar(32) COLLATE "pg_catalog"."default", - "trade_no" varchar(64) COLLATE "pg_catalog"."default", - "order_no" varchar(64) COLLATE "pg_catalog"."default", - "biz_order_no" varchar(100) COLLATE "pg_catalog"."default", - "trade_type" varchar(32) COLLATE "pg_catalog"."default", - "method" varchar(32) COLLATE "pg_catalog"."default", - "product" varchar(32) COLLATE "pg_catalog"."default", - "channel" varchar(32) COLLATE "pg_catalog"."default", - "client_ip" varchar(64) COLLATE "pg_catalog"."default", - "openid" varchar(128) COLLATE "pg_catalog"."default", - "buyer_id" varchar(128) COLLATE "pg_catalog"."default", - "scene" varchar(32) COLLATE "pg_catalog"."default", - "handle_status" varchar(32) COLLATE "pg_catalog"."default" NOT NULL DEFAULT 'pending', - "handle_remark" varchar(500) COLLATE "pg_catalog"."default", - "handle_user_id" int8, - "handle_time" timestamptz(6), - "remark" varchar(255) COLLATE "pg_catalog"."default", - "creator" int8, - "create_time" timestamptz(6), - "last_modifier" int8, - "last_modified_time" timestamptz(6), - "version" int4 DEFAULT 0, - "deleted" bool NOT NULL DEFAULT false -); - -COMMENT ON TABLE "public"."pay_risk_hit" IS '支付风险命中记录'; -COMMENT ON COLUMN "public"."pay_risk_hit"."id" IS '主键'; -COMMENT ON COLUMN "public"."pay_risk_hit"."phase" IS '阶段: before_pay/after_pay'; -COMMENT ON COLUMN "public"."pay_risk_hit"."hit_type" IS '命中类型: ip/open_id'; -COMMENT ON COLUMN "public"."pay_risk_hit"."hit_value" IS '命中值快照'; -COMMENT ON COLUMN "public"."pay_risk_hit"."blacklist_id" IS '关联黑名单ID'; -COMMENT ON COLUMN "public"."pay_risk_hit"."mch_no" IS '商户号'; -COMMENT ON COLUMN "public"."pay_risk_hit"."app_id" IS '应用号'; -COMMENT ON COLUMN "public"."pay_risk_hit"."trade_no" IS '平台交易号'; -COMMENT ON COLUMN "public"."pay_risk_hit"."order_no" IS '容器单号'; -COMMENT ON COLUMN "public"."pay_risk_hit"."biz_order_no" IS '商户业务单号'; -COMMENT ON COLUMN "public"."pay_risk_hit"."trade_type" IS '交易类型'; -COMMENT ON COLUMN "public"."pay_risk_hit"."method" IS '支付方式'; -COMMENT ON COLUMN "public"."pay_risk_hit"."product" IS '支付产品'; -COMMENT ON COLUMN "public"."pay_risk_hit"."channel" IS '支付通道'; -COMMENT ON COLUMN "public"."pay_risk_hit"."client_ip" IS '客户端IP快照'; -COMMENT ON COLUMN "public"."pay_risk_hit"."openid" IS '下单openId快照'; -COMMENT ON COLUMN "public"."pay_risk_hit"."buyer_id" IS '通道buyerId快照'; -COMMENT ON COLUMN "public"."pay_risk_hit"."scene" IS '来源场景: api/gateway/code/manual'; -COMMENT ON COLUMN "public"."pay_risk_hit"."handle_status" IS '处理状态: pending/ignored/added_blacklist/merchant_disabled/other'; -COMMENT ON COLUMN "public"."pay_risk_hit"."handle_remark" IS '处理说明'; -COMMENT ON COLUMN "public"."pay_risk_hit"."handle_user_id" IS '处理人ID'; -COMMENT ON COLUMN "public"."pay_risk_hit"."handle_time" IS '处理时间'; -COMMENT ON COLUMN "public"."pay_risk_hit"."remark" IS '备注'; -COMMENT ON COLUMN "public"."pay_risk_hit"."creator" IS '创建者ID'; -COMMENT ON COLUMN "public"."pay_risk_hit"."create_time" IS '创建时间'; -COMMENT ON COLUMN "public"."pay_risk_hit"."last_modifier" IS '最后修改者ID'; -COMMENT ON COLUMN "public"."pay_risk_hit"."last_modified_time" IS '最后修改时间'; -COMMENT ON COLUMN "public"."pay_risk_hit"."version" IS '版本号'; -COMMENT ON COLUMN "public"."pay_risk_hit"."deleted" IS '删除标志'; - -ALTER TABLE "public"."pay_risk_hit" DROP CONSTRAINT IF EXISTS "pay_risk_hit_pkey"; -ALTER TABLE "public"."pay_risk_hit" ADD CONSTRAINT "pay_risk_hit_pkey" PRIMARY KEY ("id"); - -CREATE INDEX IF NOT EXISTS "idx_pay_risk_hit_handle_time" ON "public"."pay_risk_hit" ("handle_status", "create_time"); -CREATE INDEX IF NOT EXISTS "idx_pay_risk_hit_type_value" ON "public"."pay_risk_hit" ("hit_type", "hit_value"); -CREATE INDEX IF NOT EXISTS "idx_pay_risk_hit_mch_no" ON "public"."pay_risk_hit" ("mch_no"); -CREATE INDEX IF NOT EXISTS "idx_pay_risk_hit_trade_no" ON "public"."pay_risk_hit" ("trade_no"); -CREATE INDEX IF NOT EXISTS "idx_pay_risk_hit_order_no" ON "public"."pay_risk_hit" ("order_no"); - --- ---------------------------- --- 敏感词词库 + 命中审计(capability-sensitive-word) --- ---------------------------- -CREATE TABLE IF NOT EXISTS "public"."system_sensitive_word" ( - "id" int8 NOT NULL, - "word" varchar(64) COLLATE "pg_catalog"."default" NOT NULL, - "category" varchar(32) COLLATE "pg_catalog"."default", - "match_mode" varchar(16) COLLATE "pg_catalog"."default" NOT NULL DEFAULT 'contains', - "level" varchar(16) COLLATE "pg_catalog"."default" DEFAULT 'reject', - "status" varchar(16) COLLATE "pg_catalog"."default" NOT NULL, - "remark" varchar(255) COLLATE "pg_catalog"."default", - "creator" int8, - "create_time" timestamptz(6), - "last_modifier" int8, - "last_modified_time" timestamptz(6), - "version" int4 DEFAULT 0, - "deleted" bool NOT NULL DEFAULT false -); - -COMMENT ON TABLE "public"."system_sensitive_word" IS '敏感词词库'; -COMMENT ON COLUMN "public"."system_sensitive_word"."id" IS '主键'; -COMMENT ON COLUMN "public"."system_sensitive_word"."word" IS '敏感词原文(建议简体)'; -COMMENT ON COLUMN "public"."system_sensitive_word"."category" IS '分类: politic/porn/violence/ad/custom'; -COMMENT ON COLUMN "public"."system_sensitive_word"."match_mode" IS '匹配模式: contains/exact'; -COMMENT ON COLUMN "public"."system_sensitive_word"."level" IS '处理级别: reject/warn'; -COMMENT ON COLUMN "public"."system_sensitive_word"."status" IS '状态: enable/disable'; -COMMENT ON COLUMN "public"."system_sensitive_word"."remark" IS '备注'; -COMMENT ON COLUMN "public"."system_sensitive_word"."creator" IS '创建者ID'; -COMMENT ON COLUMN "public"."system_sensitive_word"."create_time" IS '创建时间'; -COMMENT ON COLUMN "public"."system_sensitive_word"."last_modifier" IS '最后修改者ID'; -COMMENT ON COLUMN "public"."system_sensitive_word"."last_modified_time" IS '最后修改时间'; -COMMENT ON COLUMN "public"."system_sensitive_word"."version" IS '版本号'; -COMMENT ON COLUMN "public"."system_sensitive_word"."deleted" IS '删除标志'; - -ALTER TABLE "public"."system_sensitive_word" DROP CONSTRAINT IF EXISTS "system_sensitive_word_pkey"; -ALTER TABLE "public"."system_sensitive_word" ADD CONSTRAINT "system_sensitive_word_pkey" PRIMARY KEY ("id"); - -CREATE UNIQUE INDEX IF NOT EXISTS "uk_system_sensitive_word_word" ON "public"."system_sensitive_word" ("word") WHERE deleted = false; -CREATE INDEX IF NOT EXISTS "idx_system_sensitive_word_status" ON "public"."system_sensitive_word" ("status"); - -CREATE TABLE IF NOT EXISTS "public"."system_sensitive_word_hit" ( - "id" int8 NOT NULL, - "word_id" int8, - "hit_word" varchar(64) COLLATE "pg_catalog"."default" NOT NULL, - "content_preview" varchar(200) COLLATE "pg_catalog"."default", - "scene" varchar(32) COLLATE "pg_catalog"."default" NOT NULL, - "source" varchar(32) COLLATE "pg_catalog"."default", - "mch_no" varchar(32) COLLATE "pg_catalog"."default", - "app_id" varchar(50) COLLATE "pg_catalog"."default", - "operator_id" int8, - "client_ip" varchar(64) COLLATE "pg_catalog"."default", - "request_path" varchar(255) COLLATE "pg_catalog"."default", - "remark" varchar(255) COLLATE "pg_catalog"."default", - "creator" int8, - "create_time" timestamptz(6), - "last_modifier" int8, - "last_modified_time" timestamptz(6), - "version" int4 DEFAULT 0, - "deleted" bool NOT NULL DEFAULT false -); - -COMMENT ON TABLE "public"."system_sensitive_word_hit" IS '敏感词命中记录'; -COMMENT ON COLUMN "public"."system_sensitive_word_hit"."id" IS '主键'; -COMMENT ON COLUMN "public"."system_sensitive_word_hit"."word_id" IS '关联词库ID'; -COMMENT ON COLUMN "public"."system_sensitive_word_hit"."hit_word" IS '命中词快照'; -COMMENT ON COLUMN "public"."system_sensitive_word_hit"."content_preview" IS '原文摘要'; -COMMENT ON COLUMN "public"."system_sensitive_word_hit"."scene" IS '场景: pay_title/goods_name/...'; -COMMENT ON COLUMN "public"."system_sensitive_word_hit"."source" IS '来源: admin/merchant/unipay/app_admin'; -COMMENT ON COLUMN "public"."system_sensitive_word_hit"."mch_no" IS '商户号'; -COMMENT ON COLUMN "public"."system_sensitive_word_hit"."app_id" IS '应用号'; -COMMENT ON COLUMN "public"."system_sensitive_word_hit"."operator_id" IS '操作人用户ID'; -COMMENT ON COLUMN "public"."system_sensitive_word_hit"."client_ip" IS '客户端IP'; -COMMENT ON COLUMN "public"."system_sensitive_word_hit"."request_path" IS '请求路径'; -COMMENT ON COLUMN "public"."system_sensitive_word_hit"."remark" IS '备注'; -COMMENT ON COLUMN "public"."system_sensitive_word_hit"."creator" IS '创建者ID'; -COMMENT ON COLUMN "public"."system_sensitive_word_hit"."create_time" IS '创建时间'; -COMMENT ON COLUMN "public"."system_sensitive_word_hit"."last_modifier" IS '最后修改者ID'; -COMMENT ON COLUMN "public"."system_sensitive_word_hit"."last_modified_time" IS '最后修改时间'; -COMMENT ON COLUMN "public"."system_sensitive_word_hit"."version" IS '版本号'; -COMMENT ON COLUMN "public"."system_sensitive_word_hit"."deleted" IS '删除标志'; - -ALTER TABLE "public"."system_sensitive_word_hit" DROP CONSTRAINT IF EXISTS "system_sensitive_word_hit_pkey"; -ALTER TABLE "public"."system_sensitive_word_hit" ADD CONSTRAINT "system_sensitive_word_hit_pkey" PRIMARY KEY ("id"); - -CREATE INDEX IF NOT EXISTS "idx_system_sensitive_word_hit_time" ON "public"."system_sensitive_word_hit" ("create_time"); -CREATE INDEX IF NOT EXISTS "idx_system_sensitive_word_hit_word" ON "public"."system_sensitive_word_hit" ("hit_word"); -CREATE INDEX IF NOT EXISTS "idx_system_sensitive_word_hit_mch" ON "public"."system_sensitive_word_hit" ("mch_no", "create_time"); -CREATE INDEX IF NOT EXISTS "idx_system_sensitive_word_hit_scene" ON "public"."system_sensitive_word_hit" ("scene"); - --- ---------------------------- --- Table structure for pay_gateway_cashier_item --- 网关收银台支付项配置(应用级): H5 按 client_env 五档 / MINI 四档 / WEB 扁平(client_env 空) --- ---------------------------- -CREATE TABLE IF NOT EXISTS "public"."pay_gateway_cashier_item" ( - "id" int8 NOT NULL, - "mch_no" varchar(32) COLLATE "pg_catalog"."default" NOT NULL, - "app_id" varchar(32) COLLATE "pg_catalog"."default" NOT NULL, - "cashier_type" varchar(16) COLLATE "pg_catalog"."default" NOT NULL, - "client_env" varchar(32) COLLATE "pg_catalog"."default", - "name" varchar(64) COLLATE "pg_catalog"."default" NOT NULL, - "icon" varchar(32) COLLATE "pg_catalog"."default", - "recommend" bool NOT NULL DEFAULT false, - "sort_no" int4 DEFAULT 0, - "resolve_mode" varchar(16) COLLATE "pg_catalog"."default" NOT NULL, - "method" varchar(32) COLLATE "pg_catalog"."default", - "channel_mch_no" varchar(64) COLLATE "pg_catalog"."default", - "capability" varchar(64) COLLATE "pg_catalog"."default", - "creator" int8, - "create_time" timestamptz(6), - "last_modifier" int8, - "last_modified_time" timestamptz(6), - "version" int4 DEFAULT 0, - "deleted" bool NOT NULL DEFAULT false -); - -COMMENT ON TABLE "public"."pay_gateway_cashier_item" IS '网关收银台支付项配置(应用级)'; -COMMENT ON COLUMN "public"."pay_gateway_cashier_item"."id" IS '主键'; -COMMENT ON COLUMN "public"."pay_gateway_cashier_item"."mch_no" IS '商户号'; -COMMENT ON COLUMN "public"."pay_gateway_cashier_item"."app_id" IS '应用号'; -COMMENT ON COLUMN "public"."pay_gateway_cashier_item"."cashier_type" IS '收银台类型: h5/web/mini'; -COMMENT ON COLUMN "public"."pay_gateway_cashier_item"."client_env" IS '客户端环境(H5五档/MINI四档; WEB为空)'; -COMMENT ON COLUMN "public"."pay_gateway_cashier_item"."name" IS '前台展示名称'; -COMMENT ON COLUMN "public"."pay_gateway_cashier_item"."icon" IS '图标编码(与 PayProvider 对齐)'; -COMMENT ON COLUMN "public"."pay_gateway_cashier_item"."recommend" IS '是否推荐'; -COMMENT ON COLUMN "public"."pay_gateway_cashier_item"."sort_no" IS '排序号(越小越前)'; -COMMENT ON COLUMN "public"."pay_gateway_cashier_item"."resolve_mode" IS '解析模式: method/direct'; -COMMENT ON COLUMN "public"."pay_gateway_cashier_item"."method" IS '支付方式(METHOD 模式)'; -COMMENT ON COLUMN "public"."pay_gateway_cashier_item"."channel_mch_no" IS '通道商户号(DIRECT 模式)'; -COMMENT ON COLUMN "public"."pay_gateway_cashier_item"."capability" IS '支付能力(DIRECT 模式)'; -COMMENT ON COLUMN "public"."pay_gateway_cashier_item"."creator" IS '创建者ID'; -COMMENT ON COLUMN "public"."pay_gateway_cashier_item"."create_time" IS '创建时间'; -COMMENT ON COLUMN "public"."pay_gateway_cashier_item"."last_modifier" IS '最后修改者ID'; -COMMENT ON COLUMN "public"."pay_gateway_cashier_item"."last_modified_time" IS '最后修改时间'; -COMMENT ON COLUMN "public"."pay_gateway_cashier_item"."version" IS '版本号'; -COMMENT ON COLUMN "public"."pay_gateway_cashier_item"."deleted" IS '删除标志'; - -ALTER TABLE "public"."pay_gateway_cashier_item" DROP CONSTRAINT IF EXISTS "pay_gateway_cashier_item_pkey"; -ALTER TABLE "public"."pay_gateway_cashier_item" ADD CONSTRAINT "pay_gateway_cashier_item_pkey" PRIMARY KEY ("id"); - -CREATE INDEX IF NOT EXISTS "idx_pay_gateway_cashier_item_bucket" - ON "public"."pay_gateway_cashier_item" ("app_id", "cashier_type", "client_env"); -CREATE INDEX IF NOT EXISTS "idx_pay_gateway_cashier_item_mch" - ON "public"."pay_gateway_cashier_item" ("mch_no", "app_id"); - --- ---------------------------- --- Table structure for starter_audit_unipay_log(统一支付接口审计日志) --- 索引键:mch_no + req_id;不单独存 appId/业务单号(可在 body 中查看) --- ---------------------------- -CREATE TABLE IF NOT EXISTS "public"."starter_audit_unipay_log" ( - "id" int8 NOT NULL, - "mch_no" varchar(32) COLLATE "pg_catalog"."default", - "req_id" varchar(64) COLLATE "pg_catalog"."default", - "api_path" varchar(256) COLLATE "pg_catalog"."default", - "api_title" varchar(64) COLLATE "pg_catalog"."default", - "request_method" varchar(16) COLLATE "pg_catalog"."default", - "client_ip" varchar(64) COLLATE "pg_catalog"."default", - "request_ip" varchar(64) COLLATE "pg_catalog"."default", - "request_location" varchar(128) COLLATE "pg_catalog"."default", - "success" bool, - "error_code" int4, - "error_msg" varchar(512) COLLATE "pg_catalog"."default", - "duration_ms" int8, - "trace_id" varchar(64) COLLATE "pg_catalog"."default", - "req_param" jsonb, - "res_body" jsonb, - "operate_time" timestamptz(6) -); - -COMMENT ON TABLE "public"."starter_audit_unipay_log" IS '统一支付接口审计日志'; -COMMENT ON COLUMN "public"."starter_audit_unipay_log"."id" IS '主键'; -COMMENT ON COLUMN "public"."starter_audit_unipay_log"."mch_no" IS '商户号'; -COMMENT ON COLUMN "public"."starter_audit_unipay_log"."req_id" IS '请求ID(商户传入,审计主索引)'; -COMMENT ON COLUMN "public"."starter_audit_unipay_log"."api_path" IS '接口路径'; -COMMENT ON COLUMN "public"."starter_audit_unipay_log"."api_title" IS '接口标题'; -COMMENT ON COLUMN "public"."starter_audit_unipay_log"."request_method" IS 'HTTP方法'; -COMMENT ON COLUMN "public"."starter_audit_unipay_log"."client_ip" IS '商户入参声明的客户端IP'; -COMMENT ON COLUMN "public"."starter_audit_unipay_log"."request_ip" IS '真实接入IP'; -COMMENT ON COLUMN "public"."starter_audit_unipay_log"."request_location" IS '接入IP归属地'; -COMMENT ON COLUMN "public"."starter_audit_unipay_log"."success" IS '是否成功'; -COMMENT ON COLUMN "public"."starter_audit_unipay_log"."error_code" IS '业务错误码'; -COMMENT ON COLUMN "public"."starter_audit_unipay_log"."error_msg" IS '错误信息'; -COMMENT ON COLUMN "public"."starter_audit_unipay_log"."duration_ms" IS '耗时毫秒'; -COMMENT ON COLUMN "public"."starter_audit_unipay_log"."trace_id" IS '链路追踪ID'; -COMMENT ON COLUMN "public"."starter_audit_unipay_log"."req_param" IS '请求参数(脱敏后)'; -COMMENT ON COLUMN "public"."starter_audit_unipay_log"."res_body" IS '响应体(脱敏后)'; -COMMENT ON COLUMN "public"."starter_audit_unipay_log"."operate_time" IS '操作时间UTC'; - -ALTER TABLE "public"."starter_audit_unipay_log" DROP CONSTRAINT IF EXISTS "starter_audit_unipay_log_pkey"; -ALTER TABLE "public"."starter_audit_unipay_log" ADD CONSTRAINT "starter_audit_unipay_log_pkey" PRIMARY KEY ("id"); - --- 若已按旧版建表:补 req_id、删业务键列(审计表可重建,无历史依赖) -ALTER TABLE "public"."starter_audit_unipay_log" ADD COLUMN IF NOT EXISTS "req_id" varchar(64); -ALTER TABLE "public"."starter_audit_unipay_log" DROP COLUMN IF EXISTS "app_id"; -ALTER TABLE "public"."starter_audit_unipay_log" DROP COLUMN IF EXISTS "channel_mch_no"; -ALTER TABLE "public"."starter_audit_unipay_log" DROP COLUMN IF EXISTS "biz_order_no"; -ALTER TABLE "public"."starter_audit_unipay_log" DROP COLUMN IF EXISTS "order_no"; -ALTER TABLE "public"."starter_audit_unipay_log" DROP COLUMN IF EXISTS "trade_no"; - -DROP INDEX IF EXISTS "idx_starter_audit_unipay_log_biz_order"; -CREATE INDEX IF NOT EXISTS "idx_starter_audit_unipay_log_time" ON "public"."starter_audit_unipay_log" ("operate_time" DESC); -CREATE INDEX IF NOT EXISTS "idx_starter_audit_unipay_log_mch_time" ON "public"."starter_audit_unipay_log" ("mch_no", "operate_time" DESC); -CREATE INDEX IF NOT EXISTS "idx_starter_audit_unipay_log_mch_req" ON "public"."starter_audit_unipay_log" ("mch_no", "req_id"); -CREATE INDEX IF NOT EXISTS "idx_starter_audit_unipay_log_req" ON "public"."starter_audit_unipay_log" ("req_id"); -CREATE INDEX IF NOT EXISTS "idx_starter_audit_unipay_log_trace" ON "public"."starter_audit_unipay_log" ("trace_id"); -CREATE INDEX IF NOT EXISTS "idx_starter_audit_unipay_log_success_time" ON "public"."starter_audit_unipay_log" ("success", "operate_time" DESC); - --- ---------------------------- --- Table structure for pay_refund_order(退款订单) --- 对齐实体 RefundOrder:trade_no + trade_type + relation_order_no,无 method --- capability 仅通道凭证组装运行时使用,管理端不展示 --- ---------------------------- -CREATE TABLE IF NOT EXISTS "public"."pay_refund_order" ( - "id" int8 NOT NULL, - "mch_no" varchar(32) COLLATE "pg_catalog"."default", - "app_id" varchar(32) COLLATE "pg_catalog"."default", - "refund_no" varchar(64) COLLATE "pg_catalog"."default" NOT NULL, - "biz_refund_no" varchar(100) COLLATE "pg_catalog"."default", - "relation_order_no" varchar(100) COLLATE "pg_catalog"."default", - "trade_no" varchar(64) COLLATE "pg_catalog"."default" NOT NULL, - "trade_type" varchar(32) COLLATE "pg_catalog"."default", - "biz_order_no" varchar(100) COLLATE "pg_catalog"."default", - "out_order_no" varchar(150) COLLATE "pg_catalog"."default", - "title" varchar(200) COLLATE "pg_catalog"."default", - "out_refund_no" varchar(150) COLLATE "pg_catalog"."default", - "amount" int8 NOT NULL, - "order_amount" int8, - "currency" varchar(8) COLLATE "pg_catalog"."default", - "reason" varchar(256) COLLATE "pg_catalog"."default", - "status" varchar(32) COLLATE "pg_catalog"."default" NOT NULL, - "finish_time" timestamptz(6), - "error_msg" varchar(500) COLLATE "pg_catalog"."default", - "channel" varchar(32) COLLATE "pg_catalog"."default", - "product" varchar(64) COLLATE "pg_catalog"."default", - "channel_mch_no" varchar(64) COLLATE "pg_catalog"."default", - "capability" varchar(64) COLLATE "pg_catalog"."default", - "channel_app_id" varchar(64) COLLATE "pg_catalog"."default", - "notify_url" varchar(256) COLLATE "pg_catalog"."default", - "attach" varchar(512) COLLATE "pg_catalog"."default", - "client_ip" varchar(64) COLLATE "pg_catalog"."default", - "store_no" varchar(64) COLLATE "pg_catalog"."default", - "creator" int8, - "create_time" timestamptz(6), - "last_modifier" int8, - "last_modified_time" timestamptz(6), - "version" int4 NOT NULL DEFAULT 0, - "deleted" bool NOT NULL DEFAULT false -); - -COMMENT ON TABLE "public"."pay_refund_order" IS '退款订单'; -COMMENT ON COLUMN "public"."pay_refund_order"."id" IS '主键'; -COMMENT ON COLUMN "public"."pay_refund_order"."mch_no" IS '商户号'; -COMMENT ON COLUMN "public"."pay_refund_order"."app_id" IS '应用号'; -COMMENT ON COLUMN "public"."pay_refund_order"."refund_no" IS '平台退款号'; -COMMENT ON COLUMN "public"."pay_refund_order"."biz_refund_no" IS '商户退款号'; -COMMENT ON COLUMN "public"."pay_refund_order"."relation_order_no" IS '实际上送通道的商户退款关联号(普通=refund_no,特殊通道可变形)'; -COMMENT ON COLUMN "public"."pay_refund_order"."trade_no" IS '原支付资金交易号(pay_trade.trade_no)'; -COMMENT ON COLUMN "public"."pay_refund_order"."trade_type" IS '原支付交易形态(normal/gateway等, 冗余自 pay_trade.trade_type)'; -COMMENT ON COLUMN "public"."pay_refund_order"."biz_order_no" IS '商户业务订单号(冗余)'; -COMMENT ON COLUMN "public"."pay_refund_order"."out_order_no" IS '通道原支付订单号'; -COMMENT ON COLUMN "public"."pay_refund_order"."title" IS '标题(冗余自原支付)'; -COMMENT ON COLUMN "public"."pay_refund_order"."out_refund_no" IS '通道退款流水号'; -COMMENT ON COLUMN "public"."pay_refund_order"."amount" IS '退款金额(分)'; -COMMENT ON COLUMN "public"."pay_refund_order"."order_amount" IS '原支付总金额(分)'; -COMMENT ON COLUMN "public"."pay_refund_order"."currency" IS '币种'; -COMMENT ON COLUMN "public"."pay_refund_order"."reason" IS '退款原因'; -COMMENT ON COLUMN "public"."pay_refund_order"."status" IS '退款状态: progress/success/fail/close'; -COMMENT ON COLUMN "public"."pay_refund_order"."finish_time" IS '退款完成时间'; -COMMENT ON COLUMN "public"."pay_refund_order"."error_msg" IS '错误信息'; -COMMENT ON COLUMN "public"."pay_refund_order"."channel" IS '支付通道'; -COMMENT ON COLUMN "public"."pay_refund_order"."product" IS '支付产品编码'; -COMMENT ON COLUMN "public"."pay_refund_order"."channel_mch_no" IS '通道商户号'; -COMMENT ON COLUMN "public"."pay_refund_order"."capability" IS '支付能力编码'; -COMMENT ON COLUMN "public"."pay_refund_order"."channel_app_id" IS '通道应用AppId'; -COMMENT ON COLUMN "public"."pay_refund_order"."notify_url" IS '商户异步通知地址'; -COMMENT ON COLUMN "public"."pay_refund_order"."attach" IS '商户附加参数'; -COMMENT ON COLUMN "public"."pay_refund_order"."client_ip" IS '客户端IP'; -COMMENT ON COLUMN "public"."pay_refund_order"."store_no" IS '门店号'; -COMMENT ON COLUMN "public"."pay_refund_order"."creator" IS '创建者ID'; -COMMENT ON COLUMN "public"."pay_refund_order"."create_time" IS '创建时间'; -COMMENT ON COLUMN "public"."pay_refund_order"."last_modifier" IS '最后修改者ID'; -COMMENT ON COLUMN "public"."pay_refund_order"."last_modified_time" IS '最后修改时间'; -COMMENT ON COLUMN "public"."pay_refund_order"."version" IS '版本号(乐观锁)'; -COMMENT ON COLUMN "public"."pay_refund_order"."deleted" IS '删除标志'; - -ALTER TABLE "public"."pay_refund_order" DROP CONSTRAINT IF EXISTS "pay_refund_order_pkey"; -ALTER TABLE "public"."pay_refund_order" ADD CONSTRAINT "pay_refund_order_pkey" PRIMARY KEY ("id"); - -CREATE UNIQUE INDEX IF NOT EXISTS "uk_pay_refund_order_refund_no" ON "public"."pay_refund_order" ("refund_no") WHERE deleted = false; -CREATE INDEX IF NOT EXISTS "idx_pay_refund_order_relation_no" ON "public"."pay_refund_order" ("relation_order_no"); -CREATE INDEX IF NOT EXISTS "idx_pay_refund_order_out_refund_no" ON "public"."pay_refund_order" ("out_refund_no"); -CREATE INDEX IF NOT EXISTS "idx_pay_refund_order_trade_no" ON "public"."pay_refund_order" ("trade_no"); -CREATE INDEX IF NOT EXISTS "idx_pay_refund_order_trade_type" ON "public"."pay_refund_order" ("trade_type"); -CREATE INDEX IF NOT EXISTS "idx_pay_refund_order_biz_refund_no" ON "public"."pay_refund_order" ("biz_refund_no"); -CREATE INDEX IF NOT EXISTS "idx_pay_refund_order_biz_order_no" ON "public"."pay_refund_order" ("biz_order_no"); -CREATE INDEX IF NOT EXISTS "idx_pay_refund_order_mch_time" ON "public"."pay_refund_order" ("mch_no", "create_time" DESC); - --- 若旧表已存在 order_no / method 列,迁移到新模型(幂等) -DO $$ -BEGIN - IF EXISTS ( - SELECT 1 FROM information_schema.columns - WHERE table_schema = 'public' AND table_name = 'pay_refund_order' AND column_name = 'order_no' - ) AND NOT EXISTS ( - SELECT 1 FROM information_schema.columns - WHERE table_schema = 'public' AND table_name = 'pay_refund_order' AND column_name = 'trade_no' - ) THEN - ALTER TABLE "public"."pay_refund_order" RENAME COLUMN "order_no" TO "trade_no"; - END IF; - - IF EXISTS ( - SELECT 1 FROM information_schema.columns - WHERE table_schema = 'public' AND table_name = 'pay_refund_order' AND column_name = 'method' - ) THEN - ALTER TABLE "public"."pay_refund_order" DROP COLUMN "method"; - END IF; - - IF NOT EXISTS ( - SELECT 1 FROM information_schema.columns - WHERE table_schema = 'public' AND table_name = 'pay_refund_order' AND column_name = 'relation_order_no' - ) THEN - ALTER TABLE "public"."pay_refund_order" ADD COLUMN "relation_order_no" varchar(100); - UPDATE "public"."pay_refund_order" SET "relation_order_no" = "refund_no" WHERE "relation_order_no" IS NULL; - END IF; - - -- 交易类型:冗余自 pay_trade.trade_type - IF NOT EXISTS ( - SELECT 1 FROM information_schema.columns - WHERE table_schema = 'public' AND table_name = 'pay_refund_order' AND column_name = 'trade_type' - ) THEN - ALTER TABLE "public"."pay_refund_order" ADD COLUMN "trade_type" varchar(32); - END IF; -END $$; - -COMMENT ON COLUMN "public"."pay_refund_order"."trade_type" IS '原支付交易形态(normal/gateway等, 冗余自 pay_trade.trade_type)'; - --- 历史数据回填 trade_type -UPDATE "public"."pay_refund_order" r -SET "trade_type" = t."trade_type" -FROM "public"."pay_trade" t -WHERE r."trade_no" = t."trade_no" - AND r."trade_type" IS NULL - AND (t."deleted" = false OR t."deleted" IS NULL); - -CREATE INDEX IF NOT EXISTS "idx_pay_refund_order_trade_type" ON "public"."pay_refund_order" ("trade_type"); +-- 商户默认门店唯一索引 (同一商户至多一个 default_store=true 且未删除的记录) +CREATE UNIQUE INDEX IF NOT EXISTS uk_mch_store_info_default + ON mch_store_info (mch_no) + WHERE default_store = TRUE AND deleted = FALSE; diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/common/context/MerchantContextLoader.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/common/context/MerchantContextLoader.java index 11ae801b4..3bd2f86b8 100644 --- a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/common/context/MerchantContextLoader.java +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/common/context/MerchantContextLoader.java @@ -71,9 +71,16 @@ public class MerchantContextLoader { /// 解析应用:appId 空则取商户默认应用,否则按 appId 查询;校验启用与商户匹配。 /// 返回应用信息(含推导后的 appId),由调用方显式赋值到实体,不进线程上下文。 + /// + /// 异常分类(便于排查): + /// - 未传 appId 且无默认应用 → `error.payment.merchant.defaultAppConfigNotFound` + /// - 未传 appId 且默认应用已停用 → `error.payment.merchant.defaultAppDisabled` + /// - 显式 appId 不存在 → `error.payment.merchant.specifiedAppConfigNotFound` + /// - 显式 appId 已停用 → `pay.error.assist.mchAppNotEnabled` public MchAppInfoAccessInfo resolveApp(String mchNo, String appId) { MchAppInfoAccessInfo mchApp; - if (StrUtil.isBlank(appId)) { + boolean fallbackDefault = StrUtil.isBlank(appId); + if (fallbackDefault) { // appId 为空,取商户默认应用 mchApp = Optional.ofNullable(merchantAccessQueryService.getDefaultAppByMchNo(mchNo)) // 未找到商户默认应用配置 @@ -83,8 +90,12 @@ public class MerchantContextLoader { // 未找到指定的应用配置 .orElseThrow(() -> new ConfigNotEnableException("error.payment.merchant.specifiedAppConfigNotFound")); } - // 应用状态校验 + // 应用状态校验: 默认应用停用与指定应用停用分别提示, 便于排查 if (!Objects.equals(mchApp.getStatus(), MchAppStatusEnum.ENABLE.getCode())) { + if (fallbackDefault) { + // 商户默认应用已停用, 请先启用或转交默认标记 + throw new ConfigNotEnableException(CommonCode.FAIL_CODE, "error.payment.merchant.defaultAppDisabled"); + } // 商户应用未启用 throw new ConfigNotEnableException(CommonCode.FAIL_CODE, "pay.error.assist.mchAppNotEnabled"); } diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/merchant/dao/appinfo/MchAppInfoManager.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/merchant/dao/appinfo/MchAppInfoManager.java index cf77898d4..30021aaf8 100644 --- a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/merchant/dao/appinfo/MchAppInfoManager.java +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/merchant/dao/appinfo/MchAppInfoManager.java @@ -83,6 +83,11 @@ public class MchAppInfoManager extends BaseManager return existedByField(MchAppInfo::getAppId, appId); } + /// 商户下是否已有应用 + public boolean existsByMchNo(String mchNo) { + return existedByField(MchAppInfo::getMchNo, mchNo); + } + /// 清除默认应用 public void clearDefault(String mchNo) { lambdaUpdate() diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/merchant/entity/appinfo/MchAppInfo.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/merchant/entity/appinfo/MchAppInfo.java index cf97794a4..1276dfb72 100644 --- a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/merchant/entity/appinfo/MchAppInfo.java +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/merchant/entity/appinfo/MchAppInfo.java @@ -34,7 +34,7 @@ public class MchAppInfo extends MchBaseEntity implements ToResult new BizException(CommonCode.FAIL_CODE, "error.payment.merchant.mchNotExist")); param.setMchNo(mchNo); - // 新建应用默认为非默认,需通过 setDefault 或编辑时手动指定 + // 首应用或显式 defaultApp: 设为默认(同商户仅一个) + boolean firstApp = !mchAppInfoManager.existsByMchNo(mchNo); + boolean wantDefault = firstApp || param.isDefaultApp(); + // 先按非默认入库, 通过 setDefault 保证同商户唯一(避免并发直插默认) param.setDefaultApp(false); MchAppInfo entity = MchAppInfoConvert.CONVERT.toEntity(param); // 生成应用号 entity.setAppId(this.generateAppId()); entity.setMchNo(mchNo); mchAppInfoManager.save(entity); + if (wantDefault) { + this.setDefault(entity.getId()); + } } /// 为商户创建默认应用(启用 + defaultApp=true;名称按当前请求语言生成) @@ -176,11 +186,17 @@ public class MchAppInfoService { } /// 删除 + /// + /// 默认应用禁止删除(与 [MchStoreInfoService#delete] 对齐), 需先调用 [setDefault] 转交默认标记。 public void delete(Long id) { MchAppInfo mchApp = mchAppInfoManager.findById(id) // 商户: 商户应用不存在 .orElseThrow(() -> new ConfigNotExistException("error.payment.merchant.mchAppNotFound")); this.checkApp(mchApp); + if (mchApp.isDefaultApp()) { + // 商户: 默认应用不可删除, 请先转交默认标记 + throw new BizInfoException(CommonCode.FAIL_CODE, "error.payment.merchant.defaultAppCannotDelete"); + } mchAppInfoManager.deleteById(id); } diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/merchant/service/store/MchStoreInfoService.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/merchant/service/store/MchStoreInfoService.java index 20629acd7..05d6ef2ff 100644 --- a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/merchant/service/store/MchStoreInfoService.java +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/merchant/service/store/MchStoreInfoService.java @@ -132,11 +132,17 @@ public class MchStoreInfoService { } /// 删除 + /// + /// 默认门店禁止删除(与 [MchAppInfoService#delete] 对齐), 需先调用 [setDefault] 转交默认标记。 public void delete(Long id) { MchStoreInfo mchStore = mchStoreInfoManager.findById(id) // 商户: 门店不存在 .orElseThrow(() -> new DataNotExistException("error.payment.merchant.storeNotFound")); this.checkStore(mchStore); + if (mchStore.isDefaultStore()) { + // 商户: 默认门店不可删除, 请先转交默认标记 + throw new BizInfoException(CommonCode.FAIL_CODE, "error.payment.merchant.defaultStoreCannotDelete"); + } mchStoreInfoManager.deleteById(id); } @@ -222,21 +228,34 @@ public class MchStoreInfoService { } } - /// 解析门店号: 非空原样返回; 空则取商户**启用**的默认门店; 无默认或默认停用则 null + /// 解析门店号: 非空原样返回; 空则取商户默认门店。 + /// + /// 与 [cn.daxpay.open.payment.common.context.MerchantContextLoader#resolveApp] 对齐, + /// 默认门店不存在或已停用时**抛异常阻断下单**(替代早期返回 null 的宽松策略), + /// 保证订单 storeNo 维度完整。 + /// + /// 异常分类(便于排查): + /// - 未传 storeNo 且无默认门店 → `error.payment.merchant.defaultStoreNotConfigured` + /// - 未传 storeNo 且默认门店已停用 → `error.payment.merchant.defaultStoreDisabled` /// /// @param mchNo 商户号 /// @param storeNo 显式门店号(可空) - /// @return 解析后的门店号, 可空 + /// @return 解析后的门店号 public String resolveStoreNo(String mchNo, String storeNo) { if (StrUtil.isNotBlank(storeNo)) { return storeNo; } if (StrUtil.isBlank(mchNo)) { - return null; + // 商户: 商户未配置默认门店 + throw new BizInfoException(CommonCode.FAIL_CODE, "error.payment.merchant.defaultStoreNotConfigured"); } - return mchStoreInfoManager.findDefaultByMchNo(mchNo) - .filter(s -> Objects.equals(StoreStatusEnum.ENABLE.getCode(), s.getStatus())) - .map(MchStoreInfo::getStoreNo) - .orElse(null); + MchStoreInfo defaultStore = mchStoreInfoManager.findDefaultByMchNo(mchNo) + // 商户: 商户未配置默认门店 + .orElseThrow(() -> new BizInfoException(CommonCode.FAIL_CODE, "error.payment.merchant.defaultStoreNotConfigured")); + if (!Objects.equals(StoreStatusEnum.ENABLE.getCode(), defaultStore.getStatus())) { + // 商户: 默认门店已停用, 请先启用或转交默认标记 + throw new BizInfoException(CommonCode.FAIL_CODE, "error.payment.merchant.defaultStoreDisabled"); + } + return defaultStore.getStoreNo(); } } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/error/payment/merchant.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/error/payment/merchant.json index cb6741e3e..d1a3d38fe 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/error/payment/merchant.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/error/payment/merchant.json @@ -30,5 +30,10 @@ "wxVerifyFileContentTooLong": "WeChat domain verification file content is too long", "wxVerifyCodeDuplicate": "The verification file already exists, please do not upload again", "wxVerifyUploadNotSupported": "The current client does not support uploading WeChat domain verification file", - "wxVerifyNoMatch": "WeChat domain verification file does not belong to the current merchant" + "wxVerifyNoMatch": "WeChat domain verification file does not belong to the current merchant", + "defaultAppCannotDelete": "Default application cannot be deleted, please transfer the default flag first", + "defaultAppDisabled": "Merchant default application is disabled, please enable it or transfer the default flag", + "defaultStoreCannotDelete": "Default store cannot be deleted, please transfer the default flag first", + "defaultStoreNotConfigured": "Merchant has no default store configured, please configure one in store management", + "defaultStoreDisabled": "Merchant default store is disabled, please enable it or transfer the default flag" } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ja-JP/error/payment/merchant.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ja-JP/error/payment/merchant.json index 13974b5ba..2c5f6b79f 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ja-JP/error/payment/merchant.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ja-JP/error/payment/merchant.json @@ -30,5 +30,10 @@ "wxVerifyFileContentTooLong": "WeChat ドメイン検証ファイルの内容が長すぎます", "wxVerifyCodeDuplicate": "検証ファイルはすでに存在します。再度アップロードしないでください。", "wxVerifyUploadNotSupported": "現在のクライアントは WeChat ドメイン検証ファイルのアップロードをサポートしていません", - "wxVerifyNoMatch": "WeChat ドメイン検証ファイルは現在の販売者に属していません" + "wxVerifyNoMatch": "WeChat ドメイン検証ファイルは現在の販売者に属していません", + "defaultAppCannotDelete": "デフォルトアプリケーションは削除できません。先にデフォルトマークを移譲してください", + "defaultAppDisabled": "販売者のデフォルトアプリケーションが無効です。有効化するか、デフォルトマークを移譲してください", + "defaultStoreCannotDelete": "デフォルトストアは削除できません。先にデフォルトマークを移譲してください", + "defaultStoreNotConfigured": "販売者にデフォルトストアが設定されていません。ストア管理で設定してください", + "defaultStoreDisabled": "販売者のデフォルトストアが無効です。有効化するか、デフォルトマークを移譲してください" } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ko-KR/error/payment/merchant.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ko-KR/error/payment/merchant.json index 7f50d9009..b9a5d5f2f 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ko-KR/error/payment/merchant.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ko-KR/error/payment/merchant.json @@ -30,5 +30,10 @@ "wxVerifyFileContentTooLong": "WeChat 도메인 확인 파일 콘텐츠가 너무 깁니다.", "wxVerifyCodeDuplicate": "확인 파일이 이미 존재합니다. 다시 업로드하지 마세요.", "wxVerifyUploadNotSupported": "현재 클라이언트는 WeChat 도메인 확인 파일 업로드를 지원하지 않습니다.", - "wxVerifyNoMatch": "WeChat 도메인 확인 파일은 현재 판매자의 소유가 아닙니다." + "wxVerifyNoMatch": "WeChat 도메인 확인 파일은 현재 판매자의 소유가 아닙니다.", + "defaultAppCannotDelete": "기본 애플리케이션은 삭제할 수 없습니다. 먼저 기본 마크를 양도하세요.", + "defaultAppDisabled": "판매자의 기본 애플리케이션이 비활성화되었습니다. 활성화하거나 기본 마크를 양도하세요.", + "defaultStoreCannotDelete": "기본 매장은 삭제할 수 없습니다. 먼저 기본 마크를 양도하세요.", + "defaultStoreNotConfigured": "판매자에게 기본 매장이 구성되지 않았습니다. 매장 관리에서 구성하세요.", + "defaultStoreDisabled": "판매자의 기본 매장이 비활성화되었습니다. 활성화하거나 기본 마크를 양도하세요." } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/error/payment/merchant.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/error/payment/merchant.json index 9157c31c2..6cbf3fc8b 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/error/payment/merchant.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/error/payment/merchant.json @@ -30,5 +30,10 @@ "wxVerifyFileContentTooLong": "微信域名验证文件内容过长", "wxVerifyCodeDuplicate": "该验证文件已存在,请勿重复上传", "wxVerifyUploadNotSupported": "当前终端不支持上传微信域名验证文件", - "wxVerifyNoMatch": "微信域名验证文件不属于当前商户" + "wxVerifyNoMatch": "微信域名验证文件不属于当前商户", + "defaultAppCannotDelete": "默认应用不可删除, 请先转交默认标记", + "defaultAppDisabled": "商户默认应用已停用, 请先启用或转交默认标记", + "defaultStoreCannotDelete": "默认门店不可删除, 请先转交默认标记", + "defaultStoreNotConfigured": "商户未配置默认门店, 请先在门店管理中配置", + "defaultStoreDisabled": "商户默认门店已停用, 请先启用或转交默认标记" } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-HK/error/payment/merchant.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-HK/error/payment/merchant.json index 973c13879..703ae84f3 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-HK/error/payment/merchant.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-HK/error/payment/merchant.json @@ -30,5 +30,10 @@ "wxVerifyFileContentTooLong": "微信域名驗證文件內容過長", "wxVerifyCodeDuplicate": "該驗證文件已存在,請勿重複上傳", "wxVerifyUploadNotSupported": "當前終端不支持上傳微信域名驗證文件", - "wxVerifyNoMatch": "微信域名驗證文件不屬於當前商户" + "wxVerifyNoMatch": "微信域名驗證文件不屬於當前商户", + "defaultAppCannotDelete": "預設應用不可刪除, 請先轉交預設標記", + "defaultAppDisabled": "商户預設應用已停用, 請先啟用或轉交預設標記", + "defaultStoreCannotDelete": "預設門店不可刪除, 請先轉交預設標記", + "defaultStoreNotConfigured": "商户未配置預設門店, 請先在門店管理中配置", + "defaultStoreDisabled": "商户預設門店已停用, 請先啟用或轉交預設標記" } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-TW/error/payment/merchant.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-TW/error/payment/merchant.json index b0c2a6111..a24eea5cd 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-TW/error/payment/merchant.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-TW/error/payment/merchant.json @@ -30,5 +30,10 @@ "wxVerifyFileContentTooLong": "微信域名驗證檔案內容過長", "wxVerifyCodeDuplicate": "該驗證檔案已存在,請勿重複上傳", "wxVerifyUploadNotSupported": "當前終端不支援上傳微信域名驗證檔案", - "wxVerifyNoMatch": "微信域名驗證檔案不屬於當前商戶" + "wxVerifyNoMatch": "微信域名驗證檔案不屬於當前商戶", + "defaultAppCannotDelete": "預設應用不可刪除, 請先轉交預設標記", + "defaultAppDisabled": "商戶預設應用已停用, 請先啟用或轉交預設標記", + "defaultStoreCannotDelete": "預設門店不可刪除, 請先轉交預設標記", + "defaultStoreNotConfigured": "商戶未配置預設門店, 請先在門店管理中配置", + "defaultStoreDisabled": "商戶預設門店已停用, 請先啟用或轉交預設標記" }