diff --git a/_config/sql/iam_perm_menu.sql b/_config/sql/iam_perm_menu.sql index a08791dbc..1eb8f2415 100644 --- a/_config/sql/iam_perm_menu.sql +++ b/_config/sql/iam_perm_menu.sql @@ -156,6 +156,8 @@ INSERT INTO "public"."iam_perm_menu" VALUES (90302, 9, 'device:printer', 'admin' INSERT INTO "public"."iam_perm_menu" VALUES (40105, 405, 'payment:config:product_config', 'admin', 'ProductConfig', '支付产品管理', 'Payment Product Management', 'menu.payment.config.productConfig', 'lucide:layout-grid', 'f', 'f', '/payment/config/product/ProductConfig', '/payment/config/product', NULL, 1, 'f', 't', 'f', 1, 1, 0, 'f', 'menu', NULL, NULL, NULL, NULL, NULL, NULL, '2026-06-13 00:00:00+00', '2026-06-27 13:01:24.317032+00'); INSERT INTO "public"."iam_perm_menu" VALUES (4040116, 401, 'payment:platform:provider_manage', 'admin', 'PayProviderManage', '支付渠道', 'Payment Provider', 'menu.payment.platform.provider.manage', 'lucide:wallet', 'f', 'f', '/payment/masterdata/provider/PayProviderList', '/payment/platform/pay-provider', NULL, 0.5, 'f', 't', 'f', 1, 1, 0, 'f', 'menu', NULL, NULL, NULL, NULL, NULL, NULL, '2026-06-17 10:00:31.640482+00', '2026-06-27 13:01:24.318649+00'); +INSERT INTO "public"."iam_perm_menu" VALUES (4040118, 40401, 'merchant:notify_config', 'admin', 'MchAppNotifyConfig', '通知配置', 'Notify Config', 'menu.payment.merchant.app.notifyConfig', NULL, 't', 'f', '/payment/merchant/app/MchAppNotifyConfig', '/payment/merchant/app/notify-config', NULL, 11, 'f', 't', 'f', 1, 1, 0, 'f', 'subpage', NULL, NULL, NULL, NULL, NULL, NULL, '2026-07-02 16:00:00+00', '2026-07-02 16:00:00+00'); + -- ---------------------------- -- Primary Key structure for table iam_perm_menu -- ---------------------------- diff --git a/_config/sql/update-datas.sql b/_config/sql/update-datas.sql index e69de29bb..d06978b1b 100644 --- a/_config/sql/update-datas.sql +++ b/_config/sql/update-datas.sql @@ -0,0 +1,2 @@ +-- 商户应用事件通知配置: 菜单(权限码 merchant:notify_config) +INSERT INTO "public"."iam_perm_menu" VALUES (4040118, 40401, 'merchant:notify_config', 'admin', 'MchAppNotifyConfig', '通知配置', 'Notify Config', 'menu.payment.merchant.app.notifyConfig', NULL, 't', 'f', '/payment/merchant/app/MchAppNotifyConfig', '/payment/merchant/app/notify-config', NULL, 11, 'f', 't', 'f', 1, 1, 0, 'f', 'subpage', NULL, NULL, NULL, NULL, NULL, NULL, '2026-07-02 16:00:00+00', '2026-07-02 16:00:00+00'); diff --git a/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/admin/controller/merchant/config/MchAppNotifyConfigAdminController.java b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/admin/controller/merchant/config/MchAppNotifyConfigAdminController.java new file mode 100644 index 000000000..1ea7e80a6 --- /dev/null +++ b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/admin/controller/merchant/config/MchAppNotifyConfigAdminController.java @@ -0,0 +1,43 @@ +package cn.daxpay.open.payment.admin.controller.merchant.config; + +import cn.daxpay.open.platform.core.annotation.PermCode; +import cn.daxpay.open.platform.core.rest.Res; +import cn.daxpay.open.platform.core.rest.result.Result; +import cn.daxpay.open.payment.merchant.param.config.MchAppNotifyConfigParam; +import cn.daxpay.open.payment.merchant.result.config.MchAppNotifyConfigResult; +import cn.daxpay.open.payment.merchant.service.config.MchAppNotifyConfigService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.constraints.NotBlank; +import lombok.RequiredArgsConstructor; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +/// # 商户应用事件通知配置管理控制器 +/// +@PermCode(menuCode = "merchant:notify_config") +@Validated +@Tag(name = "商户应用事件通知配置管理") +@RestController +@RequestMapping("/admin/merchant/app-notify-config") +@RequiredArgsConstructor +public class MchAppNotifyConfigAdminController { + + private final MchAppNotifyConfigService notifyConfigService; + + @PermCode(code = "view", nameCn = "商户查看", nameEn = "Merchant View") + @Operation(summary = "根据应用ID查询通知配置") + @GetMapping("/get-by-app-id") + public Result findByAppId( + @NotBlank(message = "{validation.field.appId.notBlank}") String appId) { + return Res.ok(notifyConfigService.findByAppId(appId)); + } + + @PermCode(code = "notify_config_update", nameCn = "通知配置更新", nameEn = "Notify Config Update") + @Operation(summary = "保存或更新通知配置") + @PostMapping("/save-or-update") + public Result saveOrUpdate(@RequestBody @Validated MchAppNotifyConfigParam param) { + notifyConfigService.saveOrUpdate(param); + return Res.ok(); + } +} diff --git a/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/merchant/convert/config/MchAppNotifyConfigConvert.java b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/merchant/convert/config/MchAppNotifyConfigConvert.java new file mode 100644 index 000000000..99979efe4 --- /dev/null +++ b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/merchant/convert/config/MchAppNotifyConfigConvert.java @@ -0,0 +1,24 @@ +package cn.daxpay.open.payment.merchant.convert.config; + +import cn.daxpay.open.payment.merchant.entity.config.MchAppNotifyConfig; +import cn.daxpay.open.payment.merchant.param.config.MchAppNotifyConfigParam; +import cn.daxpay.open.payment.merchant.result.config.MchAppNotifyConfigResult; +import org.mapstruct.BeanMapping; +import org.mapstruct.Mapper; +import org.mapstruct.MappingTarget; +import org.mapstruct.NullValuePropertyMappingStrategy; +import org.mapstruct.factory.Mappers; + +/// # 商户应用事件通知配置转换 +/// +@Mapper +public interface MchAppNotifyConfigConvert { + MchAppNotifyConfigConvert CONVERT = Mappers.getMapper(MchAppNotifyConfigConvert.class); + + MchAppNotifyConfig toEntity(MchAppNotifyConfigParam param); + + MchAppNotifyConfigResult toResult(MchAppNotifyConfig entity); + + @BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE) + void copy(MchAppNotifyConfigParam param, @MappingTarget MchAppNotifyConfig entity); +} diff --git a/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/merchant/dao/config/MchAppNotifyConfigManager.java b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/merchant/dao/config/MchAppNotifyConfigManager.java new file mode 100644 index 000000000..de7e87e22 --- /dev/null +++ b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/merchant/dao/config/MchAppNotifyConfigManager.java @@ -0,0 +1,18 @@ +package cn.daxpay.open.payment.merchant.dao.config; + +import cn.daxpay.open.platform.common.mybatisplus.impl.BaseManager; +import cn.daxpay.open.payment.merchant.entity.config.MchAppNotifyConfig; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +/// # 商户应用事件通知配置管理 +/// +@Repository +public class MchAppNotifyConfigManager extends BaseManager { + + /// 根据应用ID查询 + public Optional findByAppId(String appId) { + return findByField(MchAppNotifyConfig::getAppId, appId); + } +} diff --git a/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/merchant/dao/config/MchAppNotifyConfigMapper.java b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/merchant/dao/config/MchAppNotifyConfigMapper.java new file mode 100644 index 000000000..9c5467ccf --- /dev/null +++ b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/merchant/dao/config/MchAppNotifyConfigMapper.java @@ -0,0 +1,11 @@ +package cn.daxpay.open.payment.merchant.dao.config; + +import cn.daxpay.open.payment.merchant.entity.config.MchAppNotifyConfig; +import com.github.yulichang.base.MPJBaseMapper; +import org.apache.ibatis.annotations.Mapper; + +/// # 商户应用事件通知配置 +/// +@Mapper +public interface MchAppNotifyConfigMapper extends MPJBaseMapper { +} diff --git a/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/merchant/entity/config/MchAppNotifyConfig.java b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/merchant/entity/config/MchAppNotifyConfig.java new file mode 100644 index 000000000..c926a43e0 --- /dev/null +++ b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/merchant/entity/config/MchAppNotifyConfig.java @@ -0,0 +1,44 @@ +package cn.daxpay.open.payment.merchant.entity.config; + +import cn.daxpay.open.platform.common.mybatisplus.function.ToResult; +import cn.daxpay.open.payment.common.entity.merchant.MchBaseEntity; +import cn.daxpay.open.payment.merchant.convert.config.MchAppNotifyConfigConvert; +import cn.daxpay.open.payment.merchant.result.config.MchAppNotifyConfigResult; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +/// # 商户应用事件通知配置 +/// +/// 应用级通用事件通知配置, 与支付订单级回调并行, 不局限于支付领域 +@EqualsAndHashCode(callSuper = true) +@Data +@Accessors(chain = true) +@TableName("mch_app_notify_config") +public class MchAppNotifyConfig extends MchBaseEntity implements ToResult { + + /// 应用ID + private String appId; + + /// 回调地址 (https) + private String notifyUrl; + + /// 通知方式 (http-HTTP异步回调) + private String notifyWay = "http"; + + /// 订阅事件类型 (逗号分隔, TradeTypeEnum的code: pay/cashouts/settle) + private String subscribedEvents; + + /// 启用状态 + private Boolean status = false; + + /// 备注 + private String remark; + + /// 转换 + @Override + public MchAppNotifyConfigResult toResult() { + return MchAppNotifyConfigConvert.CONVERT.toResult(this); + } +} diff --git a/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/merchant/param/config/MchAppNotifyConfigParam.java b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/merchant/param/config/MchAppNotifyConfigParam.java new file mode 100644 index 000000000..60f6d9df8 --- /dev/null +++ b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/merchant/param/config/MchAppNotifyConfigParam.java @@ -0,0 +1,42 @@ +package cn.daxpay.open.payment.merchant.param.config; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.Data; +import lombok.experimental.Accessors; + +/// # 商户应用事件通知配置参数 +/// +@Data +@Accessors(chain = true) +@Schema(title = "商户应用事件通知配置参数") +public class MchAppNotifyConfigParam { + + /// 应用ID + @Schema(description = "应用ID") + @NotBlank(message = "{validation.field.appId.notBlank}") + private String appId; + + /// 回调地址 + @Schema(description = "回调地址(https)") + @Size(max = 255, message = "{validation.field.notifyUrl.size}") + private String notifyUrl; + + /// 通知方式 + @Schema(description = "通知方式(http-HTTP异步回调)") + private String notifyWay; + + /// 订阅事件类型 + @Schema(description = "订阅事件类型(逗号分隔,TradeTypeEnum的code)") + private String subscribedEvents; + + /// 启用状态 + @Schema(description = "启用状态") + private Boolean status; + + /// 备注 + @Schema(description = "备注") + @Size(max = 255, message = "{validation.field.remark.size}") + private String remark; +} diff --git a/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/merchant/result/config/MchAppNotifyConfigResult.java b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/merchant/result/config/MchAppNotifyConfigResult.java new file mode 100644 index 000000000..9d8fce947 --- /dev/null +++ b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/merchant/result/config/MchAppNotifyConfigResult.java @@ -0,0 +1,44 @@ +package cn.daxpay.open.payment.merchant.result.config; + +import cn.daxpay.open.platform.core.result.BaseResult; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +/// # 商户应用事件通知配置结果 +/// +@EqualsAndHashCode(callSuper = true) +@Data +@Accessors(chain = true) +@Schema(title = "商户应用事件通知配置结果") +public class MchAppNotifyConfigResult extends BaseResult { + + /// 商户号 + @Schema(description = "商户号") + private String mchNo; + + /// 应用ID + @Schema(description = "应用ID") + private String appId; + + /// 回调地址 + @Schema(description = "回调地址") + private String notifyUrl; + + /// 通知方式 + @Schema(description = "通知方式") + private String notifyWay; + + /// 订阅事件类型 + @Schema(description = "订阅事件类型") + private String subscribedEvents; + + /// 启用状态 + @Schema(description = "启用状态") + private Boolean status; + + /// 备注 + @Schema(description = "备注") + private String remark; +} diff --git a/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/merchant/service/config/MchAppNotifyConfigService.java b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/merchant/service/config/MchAppNotifyConfigService.java new file mode 100644 index 000000000..21f69fa85 --- /dev/null +++ b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/merchant/service/config/MchAppNotifyConfigService.java @@ -0,0 +1,55 @@ +package cn.daxpay.open.payment.merchant.service.config; + +import cn.daxpay.open.platform.core.exception.DataNotExistException; +import cn.daxpay.open.payment.merchant.convert.config.MchAppNotifyConfigConvert; +import cn.daxpay.open.payment.merchant.dao.appinfo.MchAppInfoManager; +import cn.daxpay.open.payment.merchant.dao.config.MchAppNotifyConfigManager; +import cn.daxpay.open.payment.merchant.entity.appinfo.MchAppInfo; +import cn.daxpay.open.payment.merchant.entity.config.MchAppNotifyConfig; +import cn.daxpay.open.payment.merchant.param.config.MchAppNotifyConfigParam; +import cn.daxpay.open.payment.merchant.result.config.MchAppNotifyConfigResult; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/// # 商户应用事件通知配置服务 +/// +/// 应用级通用事件通知配置, 与支付订单级回调并行, 当前版本仅维护配置数据, +/// 发送链路(任务/重试/记录)后续阶段实现 +@Slf4j +@Service +@RequiredArgsConstructor +public class MchAppNotifyConfigService { + + private final MchAppNotifyConfigManager notifyConfigManager; + + private final MchAppInfoManager mchAppInfoManager; + + /// 根据应用ID查询通知配置, 无记录时返回默认空对象(不落库) + public MchAppNotifyConfigResult findByAppId(String appId) { + return notifyConfigManager.findByAppId(appId) + .map(MchAppNotifyConfig::toResult) + .orElseGet(() -> new MchAppNotifyConfigResult() + .setAppId(appId) + .setNotifyWay("http") + .setStatus(false)); + } + + /// 保存或更新(按应用ID upsert) + public void saveOrUpdate(MchAppNotifyConfigParam param) { + // 校验应用存在并获取商户号(通知配置冗余商户号, 便于按商户过滤与鉴权) + MchAppInfo mchApp = mchAppInfoManager.findByAppId(param.getAppId()) + // 商户: 商户应用不存在 + .orElseThrow(() -> new DataNotExistException("error.payment.merchant.mchAppNotFound")); + var existing = notifyConfigManager.findByAppId(param.getAppId()); + if (existing.isPresent()) { + MchAppNotifyConfig entity = existing.get(); + MchAppNotifyConfigConvert.CONVERT.copy(param, entity); + notifyConfigManager.updateById(entity); + } else { + MchAppNotifyConfig entity = MchAppNotifyConfigConvert.CONVERT.toEntity(param); + entity.setMchNo(mchApp.getMchNo()); + notifyConfigManager.save(entity); + } + } +} diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/validation.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/validation.json index 7ad5c5496..f4d9c3b8c 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/validation.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/validation.json @@ -534,6 +534,9 @@ }, "preAuthToken": { "notBlank": "Pre-auth token cannot be blank" + }, + "remark": { + "size": "Remark cannot exceed 255 characters" } } } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/validation.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/validation.json index 7d7e7ee87..362c5d39d 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/validation.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/validation.json @@ -537,6 +537,9 @@ }, "preAuthToken": { "notBlank": "预认证令牌不能为空" + }, + "remark": { + "size": "备注不可超过255位" } } }