diff --git a/daxpay-payment/daxpay-payment-app-merchant/pom.xml b/daxpay-payment/daxpay-payment-app-merchant/pom.xml
new file mode 100644
index 000000000..199b5ae00
--- /dev/null
+++ b/daxpay-payment/daxpay-payment-app-merchant/pom.xml
@@ -0,0 +1,40 @@
+
+
+ 4.0.0
+
+ cn.daxpay.open
+ daxpay-payment
+ 4.0.0-beta1
+ ../pom.xml
+
+
+ daxpay-payment-app-merchant
+ jar
+ 商户移动端(APP/小程序)支付接口;逻辑转发至 payment-merchant / payment-admin / core Service
+
+
+
+
+ org.projectlombok
+ lombok
+ provided
+
+
+
+
+ cn.daxpay.open
+ daxpay-payment-merchant
+ ${project.version}
+
+
+
+
+ cn.daxpay.open
+ daxpay-payment-admin
+ ${project.version}
+
+
+
+
diff --git a/daxpay-payment/daxpay-payment-app-merchant/src/main/java/cn/daxpay/open/payment/app/merchant/DaxpayAppMerchantApp.java b/daxpay-payment/daxpay-payment-app-merchant/src/main/java/cn/daxpay/open/payment/app/merchant/DaxpayAppMerchantApp.java
new file mode 100644
index 000000000..6fe35fb94
--- /dev/null
+++ b/daxpay-payment/daxpay-payment-app-merchant/src/main/java/cn/daxpay/open/payment/app/merchant/DaxpayAppMerchantApp.java
@@ -0,0 +1,12 @@
+package cn.daxpay.open.payment.app.merchant;
+
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.context.annotation.ComponentScan;
+
+/// # 商户移动端(APP/小程序)装配入口
+///
+/// 本模块依赖 `daxpay-payment-merchant` 和 `daxpay-payment-admin`,通过 Service 转发复用商户端 / 运营端业务逻辑。
+@AutoConfiguration
+@ComponentScan
+public class DaxpayAppMerchantApp {
+}
diff --git a/daxpay-payment/daxpay-payment-app-merchant/src/main/java/cn/daxpay/open/payment/app/merchant/controller/channel/AppMerchantChannelMerchantController.java b/daxpay-payment/daxpay-payment-app-merchant/src/main/java/cn/daxpay/open/payment/app/merchant/controller/channel/AppMerchantChannelMerchantController.java
new file mode 100644
index 000000000..ba7f2992a
--- /dev/null
+++ b/daxpay-payment/daxpay-payment-app-merchant/src/main/java/cn/daxpay/open/payment/app/merchant/controller/channel/AppMerchantChannelMerchantController.java
@@ -0,0 +1,43 @@
+package cn.daxpay.open.payment.app.merchant.controller.channel;
+
+import cn.daxpay.open.payment.app.merchant.service.channel.AppMerchantChannelMerchantService;
+import cn.daxpay.open.payment.merchant.param.channel.ChannelMerchantQuery;
+import cn.daxpay.open.payment.merchant.result.channel.ChannelMerchantResult;
+import cn.daxpay.open.platform.core.rest.Res;
+import cn.daxpay.open.platform.core.rest.param.PageParam;
+import cn.daxpay.open.platform.core.rest.result.PageResult;
+import cn.daxpay.open.platform.core.rest.result.Result;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.validation.constraints.NotNull;
+import lombok.RequiredArgsConstructor;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/// # 通道商户管理(商户移动端)
+///
+/// 面向商户移动端的通道商户查询。业务编排委托 [AppMerchantChannelMerchantService];
+/// 商户号强制取自 PaymentContext,防越权。
+@Validated
+@Tag(name = "通道商户管理(商户移动端)")
+@RestController
+@RequestMapping("/app-merchant/channel-merchant")
+@RequiredArgsConstructor
+public class AppMerchantChannelMerchantController {
+
+ private final AppMerchantChannelMerchantService channelMerchantService;
+
+ @Operation(summary = "分页查询")
+ @GetMapping("/page")
+ public Result> page(PageParam pageParam, ChannelMerchantQuery query) {
+ return Res.ok(channelMerchantService.page(pageParam, query));
+ }
+
+ @Operation(summary = "查询详情")
+ @GetMapping("/get")
+ public Result findById(@NotNull(message = "{validation.field.id.notNull}") Long id) {
+ return Res.ok(channelMerchantService.findById(id));
+ }
+}
diff --git a/daxpay-payment/daxpay-payment-app-merchant/src/main/java/cn/daxpay/open/payment/app/merchant/controller/config/AppMerchantAppNotifyConfigController.java b/daxpay-payment/daxpay-payment-app-merchant/src/main/java/cn/daxpay/open/payment/app/merchant/controller/config/AppMerchantAppNotifyConfigController.java
new file mode 100644
index 000000000..a83225a85
--- /dev/null
+++ b/daxpay-payment/daxpay-payment-app-merchant/src/main/java/cn/daxpay/open/payment/app/merchant/controller/config/AppMerchantAppNotifyConfigController.java
@@ -0,0 +1,49 @@
+package cn.daxpay.open.payment.app.merchant.controller.config;
+
+import cn.daxpay.open.payment.app.merchant.service.config.AppMerchantAppNotifyConfigService;
+import cn.daxpay.open.payment.merchant.param.config.MchAppNotifyConfigParam;
+import cn.daxpay.open.payment.merchant.result.config.MchAppNotifyConfigResult;
+import cn.daxpay.open.platform.core.annotation.PermCode;
+import cn.daxpay.open.platform.core.code.PermCodes;
+import cn.daxpay.open.platform.core.rest.Res;
+import cn.daxpay.open.platform.core.rest.result.Result;
+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.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/// # 商户应用事件通知配置(商户移动端)
+///
+/// 面向商户移动端的应用通知配置管理。业务编排委托 [AppMerchantAppNotifyConfigService]。
+@PermCode(menuCode = PermCodes.Merchant.NotifyConfig.MENU)
+@Validated
+@Tag(name = "商户应用事件通知配置(商户移动端)")
+@RestController
+@RequestMapping("/app-merchant/merchant/app-notify-config")
+@RequiredArgsConstructor
+public class AppMerchantAppNotifyConfigController {
+
+ private final AppMerchantAppNotifyConfigService notifyConfigService;
+
+ @PermCode(code = PermCodes.Action.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 = PermCodes.Action.MANAGE)
+ @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-app-merchant/src/main/java/cn/daxpay/open/payment/app/merchant/controller/config/AppMerchantCredentialController.java b/daxpay-payment/daxpay-payment-app-merchant/src/main/java/cn/daxpay/open/payment/app/merchant/controller/config/AppMerchantCredentialController.java
new file mode 100644
index 000000000..2eecd2fbf
--- /dev/null
+++ b/daxpay-payment/daxpay-payment-app-merchant/src/main/java/cn/daxpay/open/payment/app/merchant/controller/config/AppMerchantCredentialController.java
@@ -0,0 +1,44 @@
+package cn.daxpay.open.payment.app.merchant.controller.config;
+
+import cn.daxpay.open.payment.app.merchant.service.config.AppMerchantCredentialService;
+import cn.daxpay.open.payment.merchant.param.config.MerchantCredentialParam;
+import cn.daxpay.open.payment.merchant.result.config.MerchantCredentialResult;
+import cn.daxpay.open.platform.core.rest.Res;
+import cn.daxpay.open.platform.core.rest.result.Result;
+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.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/// # 商户API对接配置(商户移动端)
+///
+/// 面向商户移动端的 API 凭证管理。业务编排委托 [AppMerchantCredentialService]。
+@Validated
+@Tag(name = "商户API对接配置(商户移动端)")
+@RestController
+@RequestMapping("/app-merchant/merchant/credential")
+@RequiredArgsConstructor
+public class AppMerchantCredentialController {
+
+ private final AppMerchantCredentialService credentialService;
+
+ @Operation(summary = "根据商户号查询")
+ @GetMapping("/get-by-mch-no")
+ public Result findByMchNo(
+ @NotBlank(message = "{validation.field.mchNo.notBlank}") String mchNo) {
+ return Res.ok(credentialService.findByMchNo(mchNo));
+ }
+
+ @Operation(summary = "更新商户API配置")
+ @PostMapping("/update")
+ public Result update(@RequestBody @Validated MerchantCredentialParam param) {
+ credentialService.update(param);
+ return Res.ok();
+ }
+}
diff --git a/daxpay-payment/daxpay-payment-app-merchant/src/main/java/cn/daxpay/open/payment/app/merchant/controller/dashboard/AppMerchantDashboardTradeController.java b/daxpay-payment/daxpay-payment-app-merchant/src/main/java/cn/daxpay/open/payment/app/merchant/controller/dashboard/AppMerchantDashboardTradeController.java
new file mode 100644
index 000000000..d2db3bcc2
--- /dev/null
+++ b/daxpay-payment/daxpay-payment-app-merchant/src/main/java/cn/daxpay/open/payment/app/merchant/controller/dashboard/AppMerchantDashboardTradeController.java
@@ -0,0 +1,144 @@
+package cn.daxpay.open.payment.app.merchant.controller.dashboard;
+
+import cn.daxpay.open.payment.app.merchant.service.dashboard.AppMerchantDashboardTradeService;
+import cn.daxpay.open.payment.merchant.result.dashboard.MchDashboardHeaderCountResult;
+import cn.daxpay.open.payment.trade.report.result.AmountRangeItemResult;
+import cn.daxpay.open.payment.trade.report.result.DimRankItemResult;
+import cn.daxpay.open.payment.trade.report.result.HourlyDistItemResult;
+import cn.daxpay.open.payment.trade.report.result.ProviderDistItemResult;
+import cn.daxpay.open.payment.trade.report.result.ProviderSuccessItemResult;
+import cn.daxpay.open.payment.trade.report.result.RefundTrendItemResult;
+import cn.daxpay.open.payment.trade.report.result.TradeOverviewResult;
+import cn.daxpay.open.payment.trade.report.result.TradeTrendItemResult;
+import cn.daxpay.open.platform.core.rest.Res;
+import cn.daxpay.open.platform.core.rest.result.Result;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.RequiredArgsConstructor;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+/// # 工作台交易统计(商户移动端)
+///
+/// 面向商户移动端工作台的交易概览统计。
+/// 不挂菜单权限码: 工作台为登录即达的页面, 任何已认证商户用户均可查看。
+@Tag(name = "工作台交易统计(商户移动端)")
+@Validated
+@RestController
+@RequestMapping("/app-merchant/dashboard/trade")
+@RequiredArgsConstructor
+public class AppMerchantDashboardTradeController {
+
+ private final AppMerchantDashboardTradeService dashboardTradeService;
+
+ @Operation(summary = "工作台头部计数(应用/门店/通道商户)")
+ @GetMapping("/header-counts")
+ public Result headerCounts() {
+ return Res.ok(dashboardTradeService.headerCounts());
+ }
+
+ /// 交易概览: 支持 today/yesterday 快捷模式 + 自定义区间模式(含环比)
+ @Operation(summary = "交易概览(今日/昨日或自定义区间)")
+ @GetMapping("/overview")
+ public Result overview(
+ @RequestParam(required = false) String date,
+ @RequestParam(required = false) String start,
+ @RequestParam(required = false) String end) {
+ if (start != null && end != null) {
+ return Res.ok(dashboardTradeService.overview(start, end));
+ }
+ return Res.ok(dashboardTradeService.overview(date != null ? date : "today"));
+ }
+
+ @Operation(summary = "交易趋势")
+ @GetMapping("/trend")
+ public Result> trend(
+ @RequestParam(defaultValue = "7") int days,
+ @RequestParam(required = false) String start,
+ @RequestParam(required = false) String end) {
+ if (start != null && end != null) {
+ return Res.ok(dashboardTradeService.trend(start, end));
+ }
+ return Res.ok(dashboardTradeService.trend(days));
+ }
+
+ @Operation(summary = "退款趋势")
+ @GetMapping("/refund-trend")
+ public Result> refundTrend(
+ @RequestParam(defaultValue = "7") int days,
+ @RequestParam(required = false) String start,
+ @RequestParam(required = false) String end) {
+ if (start != null && end != null) {
+ return Res.ok(dashboardTradeService.refundTrend(start, end));
+ }
+ return Res.ok(dashboardTradeService.refundTrend(days));
+ }
+
+ @Operation(summary = "支付渠道分布")
+ @GetMapping("/provider-dist")
+ public Result> providerDist(
+ @RequestParam(defaultValue = "30") int days,
+ @RequestParam(required = false) String start,
+ @RequestParam(required = false) String end) {
+ if (start != null && end != null) {
+ return Res.ok(dashboardTradeService.providerDist(start, end));
+ }
+ return Res.ok(dashboardTradeService.providerDist(days));
+ }
+
+ @Operation(summary = "支付渠道成功率")
+ @GetMapping("/provider-success")
+ public Result> providerSuccess(
+ @RequestParam(defaultValue = "30") int days,
+ @RequestParam(required = false) String start,
+ @RequestParam(required = false) String end) {
+ if (start != null && end != null) {
+ return Res.ok(dashboardTradeService.providerSuccess(start, end));
+ }
+ return Res.ok(dashboardTradeService.providerSuccess(days));
+ }
+
+ @Operation(summary = "时段分布(日均)")
+ @GetMapping("/hourly-dist")
+ public Result> hourlyDist(
+ @RequestParam(defaultValue = "7") int days,
+ @RequestParam(required = false) String start,
+ @RequestParam(required = false) String end) {
+ if (start != null && end != null) {
+ return Res.ok(dashboardTradeService.hourlyDist(start, end));
+ }
+ return Res.ok(dashboardTradeService.hourlyDist(days));
+ }
+
+ @Operation(summary = "金额区间分桶")
+ @GetMapping("/amount-range")
+ public Result> amountRange(
+ @RequestParam(defaultValue = "7") int days,
+ @RequestParam(required = false) String start,
+ @RequestParam(required = false) String end) {
+ if (start != null && end != null) {
+ return Res.ok(dashboardTradeService.amountRange(start, end));
+ }
+ return Res.ok(dashboardTradeService.amountRange(days));
+ }
+
+ /// 维度排行: dim=channelMch|app|store
+ @Operation(summary = "维度交易额排名(通道商户/应用/门店)")
+ @GetMapping("/dim-rank")
+ public Result> dimRank(
+ @RequestParam(defaultValue = "app") String dim,
+ @RequestParam(defaultValue = "7") int days,
+ @RequestParam(defaultValue = "10") int limit,
+ @RequestParam(required = false) String start,
+ @RequestParam(required = false) String end) {
+ if (start != null && end != null) {
+ return Res.ok(dashboardTradeService.dimRank(dim, start, end, limit));
+ }
+ return Res.ok(dashboardTradeService.dimRank(dim, days, limit));
+ }
+}
diff --git a/daxpay-payment/daxpay-payment-app-merchant/src/main/java/cn/daxpay/open/payment/app/merchant/controller/gateway/AppMerchantGatewayAggregateController.java b/daxpay-payment/daxpay-payment-app-merchant/src/main/java/cn/daxpay/open/payment/app/merchant/controller/gateway/AppMerchantGatewayAggregateController.java
new file mode 100644
index 000000000..f3cab8c24
--- /dev/null
+++ b/daxpay-payment/daxpay-payment-app-merchant/src/main/java/cn/daxpay/open/payment/app/merchant/controller/gateway/AppMerchantGatewayAggregateController.java
@@ -0,0 +1,50 @@
+package cn.daxpay.open.payment.app.merchant.controller.gateway;
+
+import cn.daxpay.open.payment.app.merchant.service.gateway.AppMerchantGatewayAggregateService;
+import cn.daxpay.open.payment.merchant.param.gateway.GatewayAggregateConfigParam;
+import cn.daxpay.open.payment.merchant.result.gateway.GatewayAggregateConfigResult;
+import cn.daxpay.open.platform.core.annotation.PermCode;
+import cn.daxpay.open.platform.core.code.PermCodes;
+import cn.daxpay.open.platform.core.rest.Res;
+import cn.daxpay.open.platform.core.rest.result.Result;
+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.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/// # 网关聚合扫码配置(商户移动端)
+///
+/// 面向商户移动端的聚合支付配置。业务编排委托 [AppMerchantGatewayAggregateService];
+/// 写操作强制当前上下文 mchNo,并通过 MchAppInfoService 校验应用归属。
+@PermCode(menuCode = PermCodes.Merchant.GatewayAggregate.MENU)
+@Validated
+@Tag(name = "网关聚合扫码配置(商户移动端)")
+@RestController
+@RequestMapping("/app-merchant/gateway/aggregate-config")
+@RequiredArgsConstructor
+public class AppMerchantGatewayAggregateController {
+
+ private final AppMerchantGatewayAggregateService gatewayAggregateService;
+
+ @PermCode(code = PermCodes.Action.VIEW)
+ @Operation(summary = "按应用查询聚合扫码配置")
+ @GetMapping("/get-by-app-id")
+ public Result getByAppId(
+ @NotBlank(message = "{validation.field.appId.notBlank}") String appId) {
+ return Res.ok(gatewayAggregateService.findByAppId(appId));
+ }
+
+ @PermCode(code = PermCodes.Action.MANAGE)
+ @Operation(summary = "保存或更新聚合扫码配置")
+ @PostMapping("/save-or-update")
+ public Result saveOrUpdate(@RequestBody @Validated GatewayAggregateConfigParam param) {
+ gatewayAggregateService.saveOrUpdate(param);
+ return Res.ok();
+ }
+}
diff --git a/daxpay-payment/daxpay-payment-app-merchant/src/main/java/cn/daxpay/open/payment/app/merchant/controller/gateway/AppMerchantGatewayCashierController.java b/daxpay-payment/daxpay-payment-app-merchant/src/main/java/cn/daxpay/open/payment/app/merchant/controller/gateway/AppMerchantGatewayCashierController.java
new file mode 100644
index 000000000..7b231ae14
--- /dev/null
+++ b/daxpay-payment/daxpay-payment-app-merchant/src/main/java/cn/daxpay/open/payment/app/merchant/controller/gateway/AppMerchantGatewayCashierController.java
@@ -0,0 +1,74 @@
+package cn.daxpay.open.payment.app.merchant.controller.gateway;
+
+import cn.daxpay.open.payment.app.merchant.service.gateway.AppMerchantGatewayCashierService;
+import cn.daxpay.open.payment.merchant.param.gateway.GatewayCashierItemParam;
+import cn.daxpay.open.payment.merchant.result.gateway.GatewayCashierItemResult;
+import cn.daxpay.open.platform.core.annotation.PermCode;
+import cn.daxpay.open.platform.core.code.PermCodes;
+import cn.daxpay.open.platform.core.rest.Res;
+import cn.daxpay.open.platform.core.rest.result.Result;
+import cn.daxpay.open.platform.core.util.ValidationUtil;
+import cn.daxpay.open.platform.core.validation.ValidationGroup;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import lombok.RequiredArgsConstructor;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+/// # 网关收银台配置(商户移动端)
+///
+/// 面向商户移动端的收银台支付项管理。业务编排委托 [AppMerchantGatewayCashierService]。
+@PermCode(menuCode = PermCodes.Merchant.GatewayCashier.MENU)
+@Validated
+@Tag(name = "网关收银台配置(商户移动端)")
+@RestController
+@RequestMapping("/app-merchant/gateway/cashier-config")
+@RequiredArgsConstructor
+public class AppMerchantGatewayCashierController {
+
+ private final AppMerchantGatewayCashierService gatewayCashierService;
+
+ @PermCode(code = PermCodes.Action.VIEW)
+ @Operation(summary = "按应用与分桶查询收银台支付项列表")
+ @GetMapping("/list")
+ public Result> list(
+ @NotBlank(message = "{validation.field.appId.notBlank}") String appId,
+ @NotBlank(message = "{validation.field.cashierType.notBlank}") String cashierType,
+ String clientEnv) {
+ return Res.ok(gatewayCashierService.list(appId, cashierType, clientEnv));
+ }
+
+ @PermCode(code = PermCodes.Action.MANAGE)
+ @Operation(summary = "新建收银台支付项")
+ @PostMapping("/save")
+ public Result save(@RequestBody @Validated GatewayCashierItemParam param) {
+ gatewayCashierService.save(param);
+ return Res.ok();
+ }
+
+ @PermCode(code = PermCodes.Action.MANAGE)
+ @Operation(summary = "更新收银台支付项")
+ @PostMapping("/update")
+ public Result update(@RequestBody @Validated(ValidationGroup.edit.class) GatewayCashierItemParam param) {
+ // 同时校验 Default 分组字段与 edit 分组主键
+ ValidationUtil.validateParam(param, jakarta.validation.groups.Default.class, ValidationGroup.edit.class);
+ gatewayCashierService.update(param);
+ return Res.ok();
+ }
+
+ @PermCode(code = PermCodes.Action.MANAGE)
+ @Operation(summary = "删除收银台支付项")
+ @PostMapping("/delete")
+ public Result delete(@NotNull(message = "{validation.field.id.notNull}") Long id) {
+ gatewayCashierService.delete(id);
+ return Res.ok();
+ }
+}
diff --git a/daxpay-payment/daxpay-payment-app-merchant/src/main/java/cn/daxpay/open/payment/app/merchant/controller/gateway/AppMerchantGatewayCodeConfigController.java b/daxpay-payment/daxpay-payment-app-merchant/src/main/java/cn/daxpay/open/payment/app/merchant/controller/gateway/AppMerchantGatewayCodeConfigController.java
new file mode 100644
index 000000000..04939b1a3
--- /dev/null
+++ b/daxpay-payment/daxpay-payment-app-merchant/src/main/java/cn/daxpay/open/payment/app/merchant/controller/gateway/AppMerchantGatewayCodeConfigController.java
@@ -0,0 +1,72 @@
+package cn.daxpay.open.payment.app.merchant.controller.gateway;
+
+import cn.daxpay.open.payment.app.merchant.service.gateway.AppMerchantGatewayCodeConfigService;
+import cn.daxpay.open.payment.merchant.param.gateway.GatewayCodeConfigParam;
+import cn.daxpay.open.payment.merchant.result.gateway.GatewayCodeConfigResult;
+import cn.daxpay.open.platform.core.annotation.PermCode;
+import cn.daxpay.open.platform.core.code.PermCodes;
+import cn.daxpay.open.platform.core.rest.Res;
+import cn.daxpay.open.platform.core.rest.dto.ChannelMchOption;
+import cn.daxpay.open.platform.core.rest.dto.LabelValue;
+import cn.daxpay.open.platform.core.rest.result.Result;
+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.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+/// # 码牌支付策略配置(商户移动端)
+///
+/// 面向商户移动端的码牌支付配置。业务编排委托 [AppMerchantGatewayCodeConfigService];
+/// 写操作强制当前上下文 mchNo,并通过 MchAppInfoService 校验应用归属。
+@PermCode(menuCode = PermCodes.Merchant.GatewayCode.MENU)
+@Validated
+@Tag(name = "码牌支付策略配置(商户移动端)")
+@RestController
+@RequestMapping("/app-merchant/gateway/code-config")
+@RequiredArgsConstructor
+public class AppMerchantGatewayCodeConfigController {
+
+ private final AppMerchantGatewayCodeConfigService gatewayCodeConfigService;
+
+ @PermCode(code = PermCodes.Action.VIEW)
+ @Operation(summary = "按应用查询码牌支付配置")
+ @GetMapping("/get-by-app-id")
+ public Result getByAppId(
+ @NotBlank(message = "{validation.field.appId.notBlank}") String appId) {
+ return Res.ok(gatewayCodeConfigService.findByAppId(appId));
+ }
+
+ @PermCode(code = PermCodes.Action.MANAGE)
+ @Operation(summary = "保存或更新码牌支付配置")
+ @PostMapping("/save-or-update")
+ public Result saveOrUpdate(@RequestBody @Validated GatewayCodeConfigParam param) {
+ gatewayCodeConfigService.saveOrUpdate(param);
+ return Res.ok();
+ }
+
+ /// DIRECT 模式: 按当前商户+支付渠道列通道商户
+ @PermCode(code = PermCodes.Action.VIEW)
+ @Operation(summary = "直接指定-通道商户候选")
+ @GetMapping("/direct-channel-mch-candidates")
+ public Result> listDirectChannelMchCandidates(
+ @NotBlank(message = "{validation.field.provider.notBlank}") String provider) {
+ return Res.ok(gatewayCodeConfigService.listDirectChannelMchCandidates(provider));
+ }
+
+ /// DIRECT 模式: 按通道商户列全部已挂载支付能力
+ @PermCode(code = PermCodes.Action.VIEW)
+ @Operation(summary = "直接指定-支付能力候选")
+ @GetMapping("/direct-capability-candidates")
+ public Result> listDirectCapabilityCandidates(
+ @NotBlank(message = "{validation.field.channelMchNo.notBlank}") String channelMchNo) {
+ return Res.ok(gatewayCodeConfigService.listDirectCapabilityCandidates(channelMchNo));
+ }
+}
diff --git a/daxpay-payment/daxpay-payment-app-merchant/src/main/java/cn/daxpay/open/payment/app/merchant/controller/info/AppMerchantAppInfoController.java b/daxpay-payment/daxpay-payment-app-merchant/src/main/java/cn/daxpay/open/payment/app/merchant/controller/info/AppMerchantAppInfoController.java
new file mode 100644
index 000000000..06060dfa5
--- /dev/null
+++ b/daxpay-payment/daxpay-payment-app-merchant/src/main/java/cn/daxpay/open/payment/app/merchant/controller/info/AppMerchantAppInfoController.java
@@ -0,0 +1,95 @@
+package cn.daxpay.open.payment.app.merchant.controller.info;
+
+import cn.daxpay.open.payment.app.merchant.service.info.AppMerchantAppInfoService;
+import cn.daxpay.open.payment.merchant.param.appinfo.MchAppInfoParam;
+import cn.daxpay.open.payment.merchant.param.appinfo.MchAppInfoQuery;
+import cn.daxpay.open.payment.merchant.result.appinfo.MchAppInfoResult;
+import cn.daxpay.open.platform.core.rest.Res;
+import cn.daxpay.open.platform.core.rest.param.PageParam;
+import cn.daxpay.open.platform.core.rest.result.PageResult;
+import cn.daxpay.open.platform.core.rest.result.Result;
+import cn.daxpay.open.platform.core.validation.ValidationGroup;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.validation.constraints.NotNull;
+import lombok.RequiredArgsConstructor;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+/// # 商户应用配置(商户移动端)
+///
+/// 面向商户移动端的应用管理。业务编排委托 [AppMerchantAppInfoService]。
+@Validated
+@Tag(name = "商户应用配置(商户移动端)")
+@RestController
+@RequestMapping("/app-merchant/app-info")
+@RequiredArgsConstructor
+public class AppMerchantAppInfoController {
+
+ private final AppMerchantAppInfoService appInfoService;
+
+ @Operation(summary = "商户应用分页")
+ @GetMapping("/page")
+ public Result> page(PageParam pageParam, MchAppInfoQuery query) {
+ return Res.ok(appInfoService.page(pageParam, query));
+ }
+
+ @Operation(summary = "商户应用列表")
+ @GetMapping("/list")
+ public Result> list() {
+ return Res.ok(appInfoService.list());
+ }
+
+ @Operation(summary = "根据id查询商户应用")
+ @GetMapping("/get")
+ public Result findById(@NotNull(message = "{validation.field.id.notNull}") Long id) {
+ return Res.ok(appInfoService.findById(id));
+ }
+
+ @Operation(summary = "根据应用AppId获取应用详情")
+ @GetMapping("/get-by-app-id")
+ public Result findByAppId(@NotNull(message = "{validation.field.appId.notBlank}") String appId) {
+ return Res.ok(appInfoService.findByAppId(appId));
+ }
+
+ @Operation(summary = "新增商户应用")
+ @PostMapping("/add")
+ public Result add(@RequestBody @Validated(ValidationGroup.add.class) MchAppInfoParam param) {
+ appInfoService.add(param);
+ return Res.ok();
+ }
+
+ @Operation(summary = "修改商户应用")
+ @PostMapping("/update")
+ public Result update(@RequestBody @Validated(ValidationGroup.edit.class) MchAppInfoParam param) {
+ appInfoService.update(param);
+ return Res.ok();
+ }
+
+ @Operation(summary = "删除商户应用")
+ @PostMapping("/delete")
+ public Result delete(@NotNull(message = "{validation.field.id.notNull}") Long id) {
+ appInfoService.delete(id);
+ return Res.ok();
+ }
+
+ @Operation(summary = "设置默认商户应用")
+ @PostMapping("/set-default")
+ public Result setDefault(@NotNull(message = "{validation.field.id.notNull}") Long id) {
+ appInfoService.setDefault(id);
+ return Res.ok();
+ }
+
+ @Operation(summary = "取消默认商户应用")
+ @PostMapping("/clear-default")
+ public Result clearDefault(@NotNull(message = "{validation.field.id.notNull}") Long id) {
+ appInfoService.clearDefault(id);
+ return Res.ok();
+ }
+}
diff --git a/daxpay-payment/daxpay-payment-app-merchant/src/main/java/cn/daxpay/open/payment/app/merchant/controller/info/AppMerchantInfoController.java b/daxpay-payment/daxpay-payment-app-merchant/src/main/java/cn/daxpay/open/payment/app/merchant/controller/info/AppMerchantInfoController.java
new file mode 100644
index 000000000..fe84bae27
--- /dev/null
+++ b/daxpay-payment/daxpay-payment-app-merchant/src/main/java/cn/daxpay/open/payment/app/merchant/controller/info/AppMerchantInfoController.java
@@ -0,0 +1,43 @@
+package cn.daxpay.open.payment.app.merchant.controller.info;
+
+import cn.daxpay.open.payment.app.merchant.service.info.AppMerchantInfoService;
+import cn.daxpay.open.payment.merchant.param.info.MerchantInfoParam;
+import cn.daxpay.open.payment.merchant.result.info.MerchantInfoResult;
+import cn.daxpay.open.platform.core.rest.Res;
+import cn.daxpay.open.platform.core.rest.result.Result;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.RequiredArgsConstructor;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/// # 商户信息(商户移动端)
+///
+/// 获取/更新当前登录商户资料。业务编排委托 [AppMerchantInfoService]。
+@Validated
+@Tag(name = "商户信息(商户移动端)")
+@RestController
+@RequestMapping("/app-merchant/merchant")
+@RequiredArgsConstructor
+public class AppMerchantInfoController {
+
+ private final AppMerchantInfoService merchantInfoService;
+
+ @Operation(summary = "获取商户信息")
+ @GetMapping("/get")
+ public Result getMerchant() {
+ return Res.ok(merchantInfoService.getMerchant());
+ }
+
+ /// 更新当前登录商户资料(mchNo 取自 PaymentContext,不信任入参)
+ @Operation(summary = "更新商户信息")
+ @PostMapping("/update")
+ public Result update(@RequestBody @Validated MerchantInfoParam param) {
+ merchantInfoService.update(param);
+ return Res.ok();
+ }
+}
diff --git a/daxpay-payment/daxpay-payment-app-merchant/src/main/java/cn/daxpay/open/payment/app/merchant/controller/route/AppMerchantPayRouteController.java b/daxpay-payment/daxpay-payment-app-merchant/src/main/java/cn/daxpay/open/payment/app/merchant/controller/route/AppMerchantPayRouteController.java
new file mode 100644
index 000000000..a9e8db3c3
--- /dev/null
+++ b/daxpay-payment/daxpay-payment-app-merchant/src/main/java/cn/daxpay/open/payment/app/merchant/controller/route/AppMerchantPayRouteController.java
@@ -0,0 +1,136 @@
+package cn.daxpay.open.payment.app.merchant.controller.route;
+
+import cn.daxpay.open.payment.app.merchant.service.route.AppMerchantPayRouteService;
+import cn.daxpay.open.payment.masterdata.result.provider.PayProviderMethodResult;
+import cn.daxpay.open.payment.route.param.basic.PayRouteBasicConfigBatchParam;
+import cn.daxpay.open.payment.route.param.scene.PayRouteSceneCapabilityBatchParam;
+import cn.daxpay.open.payment.route.param.scene.PayRouteSceneConfigBatchParam;
+import cn.daxpay.open.payment.route.param.strategy.PayRouteStrategyParam;
+import cn.daxpay.open.platform.core.annotation.PermCode;
+import cn.daxpay.open.platform.core.code.PermCodes;
+import cn.daxpay.open.platform.core.rest.Res;
+import cn.daxpay.open.platform.core.rest.dto.ChannelMchOption;
+import cn.daxpay.open.platform.core.rest.dto.LabelValue;
+import cn.daxpay.open.platform.core.rest.result.Result;
+import cn.daxpay.open.payment.route.result.basic.PayRouteBasicConfigResult;
+import cn.daxpay.open.payment.route.result.scene.PayRouteSceneConfigResult;
+import cn.daxpay.open.payment.route.result.strategy.PayRouteStrategyResult;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.NotBlank;
+import lombok.RequiredArgsConstructor;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+import java.util.Map;
+
+/// # 商户应用通道路由(商户移动端)
+///
+/// 面向商户移动端的通道路由配置。业务编排委托 [AppMerchantPayRouteService]。
+@PermCode(menuCode = PermCodes.Merchant.AppRoute.MENU)
+@Validated
+@Tag(name = "应用通道路由管理(商户移动端)")
+@RestController
+@RequestMapping("/app-merchant/pay-route")
+@RequiredArgsConstructor
+public class AppMerchantPayRouteController {
+
+ private final AppMerchantPayRouteService payRouteService;
+
+ @PermCode(code = PermCodes.Action.VIEW)
+ @Operation(summary = "已启用渠道支付方式扁平列表")
+ @GetMapping("/method-directory/flat-list")
+ public Result> listMethodDirectoryFlat() {
+ return Res.ok(payRouteService.listMethodDirectoryFlat());
+ }
+
+ @PermCode(code = PermCodes.Action.VIEW)
+ @Operation(summary = "获取或初始化应用路由策略")
+ @GetMapping("/strategy/get-or-init-by-app-id")
+ public Result getOrInitByAppId(
+ @NotBlank(message = "{validation.field.appId.notBlank}") String appId) {
+ return Res.ok(payRouteService.getOrInitByAppId(appId));
+ }
+
+ @PermCode(code = PermCodes.Action.MANAGE)
+ @Operation(summary = "更新路由策略")
+ @PostMapping("/strategy/update")
+ public Result updateStrategy(@RequestBody @Validated PayRouteStrategyParam param) {
+ return Res.ok(payRouteService.updateStrategy(param));
+ }
+
+ @PermCode(code = PermCodes.Action.VIEW)
+ @Operation(summary = "查询场景模式配置列表")
+ @GetMapping("/scene-config/list-by-app-id")
+ public Result> listSceneByAppId(
+ @NotBlank(message = "{validation.field.appId.notBlank}") String appId) {
+ return Res.ok(payRouteService.listSceneByAppId(appId));
+ }
+
+ @PermCode(code = PermCodes.Action.MANAGE)
+ @Operation(summary = "批量保存场景模式配置")
+ @PostMapping("/scene-config/save-batch")
+ public Result saveSceneBatch(@RequestBody @Validated PayRouteSceneConfigBatchParam param) {
+ payRouteService.saveSceneBatch(param);
+ return Res.ok();
+ }
+
+ @PermCode(code = PermCodes.Action.VIEW)
+ @Operation(summary = "通道路由白名单目录下全部通道商户候选(批量)")
+ @GetMapping("/scene-config/channel-mh-candidates-batch")
+ public Result