diff --git a/_config/sql/iam_perm_menu.sql b/_config/sql/iam_perm_menu.sql index f657dfe55..d0747694a 100644 --- a/_config/sql/iam_perm_menu.sql +++ b/_config/sql/iam_perm_menu.sql @@ -134,7 +134,7 @@ INSERT INTO "public"."iam_perm_menu" VALUES (90201, 9, 'device:vendor_config', ' INSERT INTO "public"."iam_perm_menu" VALUES (40503, 40105, 'payment:wechat:isv', 'admin', 'WechatIsvAppManage', '微信服务商应用', 'WeChat ISV Application', 'menu.payment.config.wechatApp', NULL, 't', 'f', '/payment/channel/wechat/manage/app/WechatIsvAppManage', '/payment/config/product/wechat-app-manage', NULL, 4, 'f', 't', 'f', 1, 1, 0, 'f', 'subpage', NULL, NULL, NULL, NULL, NULL, NULL, '2026-06-14 00:00:00+00', '2026-06-14 00:00:00+00'); INSERT INTO "public"."iam_perm_menu" VALUES (90202, 9, 'device:speaker', 'admin', 'DeviceSpeakerList', '云音箱管理', 'Speaker Management', 'menu.device.speaker', 'lucide:speaker', 'f', 'f', '/payment/device/speaker/DeviceSpeakerList', '/device/speaker', NULL, 3, 'f', 't', 'f', 1, 1, 0, 'f', 'menu', NULL, NULL, NULL, NULL, NULL, NULL, '2026-06-25 01:21:34.647632+00', '2026-06-25 12:51:25.245363+00'); INSERT INTO "public"."iam_perm_menu" VALUES (8, NULL, 'develop', 'admin', 'Develop', '开发调试', 'Develop', 'menu.develop', 'lucide:wrench', 'f', 'f', NULL, '/develop', NULL, 5, 'f', 't', 'f', 1, 1, 0, 'f', 'catalog', NULL, NULL, NULL, NULL, NULL, NULL, '2026-06-23 16:00:00+00', '2026-06-23 16:00:00+00'); -INSERT INTO "public"."iam_perm_menu" VALUES (901, 9, 'device:qrcode', 'admin', 'DeviceQrCode', '码牌管理', 'QR Code Management', 'menu.device.qrcode', 'lucide:qr-code', 't', 'f', '/_core/fallback/coming-soon', '/device/qrcode', NULL, 2, 'f', 't', 'f', 1, 1, 0, 'f', 'menu', NULL, NULL, NULL, NULL, NULL, NULL, '2026-06-25 01:21:34.634639+00', '2026-06-25 12:51:25.24304+00'); +INSERT INTO "public"."iam_perm_menu" VALUES (901, 9, 'device:qrcode', 'admin', 'DeviceQrCode', '码牌管理', 'QR Code Management', 'menu.device.qrcode', 'lucide:qr-code', 'f', 'f', '/payment/device/qrcode/DeviceQrCode', '/device/qrcode', NULL, 2, 'f', 't', 'f', 1, 1, 0, 'f', 'menu', NULL, NULL, NULL, NULL, NULL, NULL, '2026-06-25 01:21:34.634639+00', '2026-06-25 12:51:25.24304+00'); INSERT INTO "public"."iam_perm_menu" VALUES (204, 2, 'demos:artemis', 'admin', 'ArtemisDemo', '消息队列演示', 'Message Queue Demo', 'menu.demos.artemis', 'lucide:send', 'f', 'f', '/demos/artemis/ArtemisDemo', '/demos/artemis', NULL, 4, 'f', 't', 'f', 0, NULL, 0, 'f', 'menu', NULL, NULL, NULL, NULL, NULL, NULL, '2026-06-18 00:00:00+00', '2026-06-18 00:00:00+00'); INSERT INTO "public"."iam_perm_menu" VALUES (900001, 304, 'iam:social:login-config', 'admin', 'ThirdPlatform', '三方平台管理', 'Third-party Platform Management', 'menu.system.config.thirdPlatform', 'lucide:share-2', 'f', 'f', 'views/system/config/third-platform/ThirdPlatform', '/system/config/third-platform', NULL, 5, 'f', 't', 'f', 1, 1, 0, 'f', 'menu', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL); INSERT INTO "public"."iam_perm_menu" VALUES (30403, 304, 'system:protocol', 'admin', 'UserProtocol', '用户协议', 'User Protocol', 'menu.system.config.protocol', 'lucide:file-text', 'f', 'f', '/system/protocol/UserProtocolList', '/system/config/protocol', NULL, 10, 'f', 't', 'f', 1, 1, 0, 'f', 'menu', NULL, NULL, NULL, NULL, NULL, NULL, '2026-06-24 10:32:13.183371+00', '2026-06-24 10:32:13.183371+00'); diff --git a/_config/sql/update-tables.sql b/_config/sql/update-tables.sql index b4675f224..d899b5222 100644 --- a/_config/sql/update-tables.sql +++ b/_config/sql/update-tables.sql @@ -201,3 +201,13 @@ COMMENT ON COLUMN pay_platform_mobile_app.deleted IS '逻辑删除标记'; CREATE UNIQUE INDEX IF NOT EXISTS uk_pay_platform_mobile_app_type_platform ON pay_platform_mobile_app (app_type, platform) WHERE deleted = false; + +-- ------------------------------------------------------------ +-- 码牌: 支持空白库存与划拨(批次号 + 商户号可空) +-- ------------------------------------------------------------ +ALTER TABLE device_qr_code ADD COLUMN IF NOT EXISTS batch_no varchar(64); +ALTER TABLE device_qr_code ALTER COLUMN mch_no DROP NOT NULL; + +COMMENT ON COLUMN device_qr_code.batch_no IS '批次号(批量创建空白码时写入)'; +COMMENT ON COLUMN device_qr_code.mch_no IS '所属商户号(空白码为空, 划拨后写入)'; + diff --git a/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/device/enums/QrCodeAmountTypeEnum.java b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/device/enums/QrCodeAmountTypeEnum.java new file mode 100644 index 000000000..fa6b1821a --- /dev/null +++ b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/device/enums/QrCodeAmountTypeEnum.java @@ -0,0 +1,39 @@ +package cn.daxpay.open.payment.device.enums; + +import cn.daxpay.open.platform.core.exception.DataNotExistException; +import cn.daxpay.open.platform.core.i18n.I18nSupport; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; + +/// # 码牌金额类型 +/// +/// 字典: qrcode_amount_type +@Getter +@RequiredArgsConstructor +public enum QrCodeAmountTypeEnum implements I18nSupport { + + /// 自定义金额(扫码后由用户输入) + RANDOM("random"), + /// 固定金额(创建时设定, 扫码后直接支付) + FIXED("fixed"); + + /// 编码 + private final String code; + + /// 翻译 key 前缀 + @Override + public String getI18nPrefix() { + return "enum.qrcode_amount_type"; + } + + /// 根据编码查找 + public static QrCodeAmountTypeEnum findByCode(String code) { + return Arrays.stream(values()) + .filter(e -> e.getCode().equals(code)) + .findFirst() + // 通用: 未找到对应的码牌金额类型: {0} + .orElseThrow(() -> new DataNotExistException("error.device.qrcode.amountTypeNotFound", code)); + } +} diff --git a/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/device/enums/QrCodeStatusEnum.java b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/device/enums/QrCodeStatusEnum.java new file mode 100644 index 000000000..834b5bbca --- /dev/null +++ b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/device/enums/QrCodeStatusEnum.java @@ -0,0 +1,39 @@ +package cn.daxpay.open.payment.device.enums; + +import cn.daxpay.open.platform.core.exception.DataNotExistException; +import cn.daxpay.open.platform.core.i18n.I18nSupport; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; + +/// # 码牌状态 +/// +/// 字典: qrcode_status +@Getter +@RequiredArgsConstructor +public enum QrCodeStatusEnum implements I18nSupport { + + /// 启用 + ENABLED("enabled"), + /// 停用 + DISABLED("disabled"); + + /// 编码 + private final String code; + + /// 翻译 key 前缀 + @Override + public String getI18nPrefix() { + return "enum.qrcode_status"; + } + + /// 根据编码查找 + public static QrCodeStatusEnum findByCode(String code) { + return Arrays.stream(values()) + .filter(e -> e.getCode().equals(code)) + .findFirst() + // 通用: 未找到对应的码牌状态: {0} + .orElseThrow(() -> new DataNotExistException("error.device.qrcode.statusNotFound", code)); + } +} diff --git a/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/device/qrcode/controller/admin/DeviceQrCodeAdminController.java b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/device/qrcode/controller/admin/DeviceQrCodeAdminController.java new file mode 100644 index 000000000..b94dd4ae3 --- /dev/null +++ b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/device/qrcode/controller/admin/DeviceQrCodeAdminController.java @@ -0,0 +1,119 @@ +package cn.daxpay.open.payment.device.qrcode.controller.admin; + +import cn.daxpay.open.payment.device.qrcode.param.DeviceQrCodeBatchParam; +import cn.daxpay.open.payment.device.qrcode.param.DeviceQrCodeBindMerchantParam; +import cn.daxpay.open.payment.device.qrcode.param.DeviceQrCodeParam; +import cn.daxpay.open.payment.device.qrcode.param.DeviceQrCodeQuery; +import cn.daxpay.open.payment.device.qrcode.result.DeviceQrCodeResult; +import cn.daxpay.open.payment.device.qrcode.service.DeviceQrCodeAdminService; +import cn.daxpay.open.platform.core.annotation.PermCode; +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.NotBlank; +import jakarta.validation.constraints.NotEmpty; +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; + +/// # 支付码牌管理(运营端) +/// +@PermCode(menuCode = "device:qrcode") +@Validated +@Tag(name = "支付码牌管理") +@RestController +@RequestMapping("/admin/device/qrcode") +@RequiredArgsConstructor +public class DeviceQrCodeAdminController { + + private final DeviceQrCodeAdminService deviceQrCodeAdminService; + + @PermCode(code = "manage", nameCn = "码牌管理", nameEn = "QrCode Manage") + @Operation(summary = "新增码牌(快捷绑定商户)") + @PostMapping("/add") + public Result add(@RequestBody @Validated(ValidationGroup.add.class) DeviceQrCodeParam param) { + deviceQrCodeAdminService.add(param); + return Res.ok(); + } + + @PermCode(code = "manage", nameCn = "码牌管理", nameEn = "QrCode Manage") + @Operation(summary = "批量创建空白码牌") + @PostMapping("/create-batch") + public Result createBatch(@RequestBody @Validated DeviceQrCodeBatchParam param) { + deviceQrCodeAdminService.createBatch(param); + return Res.ok(); + } + + @PermCode(code = "view", nameCn = "码牌查看", nameEn = "QrCode View") + @Operation(summary = "判断批次号是否已存在") + @GetMapping("/exists-by-batch-no") + public Result existsByBatchNo(@NotBlank(message = "{validation.field.batchNo.notBlank}") String batchNo) { + return Res.ok(deviceQrCodeAdminService.existsByBatchNo(batchNo)); + } + + @PermCode(code = "manage", nameCn = "码牌管理", nameEn = "QrCode Manage") + @Operation(summary = "批量绑定商户") + @PostMapping("/bind-merchant") + public Result bindMerchant(@RequestBody @Validated DeviceQrCodeBindMerchantParam param) { + deviceQrCodeAdminService.bindMerchant(param); + return Res.ok(); + } + + @PermCode(code = "manage", nameCn = "码牌管理", nameEn = "QrCode Manage") + @Operation(summary = "批量解绑商户") + @PostMapping("/unbind-merchant") + public Result unbindMerchant(@RequestBody @NotEmpty(message = "{validation.field.ids.notEmpty}") List ids) { + deviceQrCodeAdminService.unbindMerchant(ids); + return Res.ok(); + } + + @PermCode(code = "manage", nameCn = "码牌管理", nameEn = "QrCode Manage") + @Operation(summary = "修改码牌") + @PostMapping("/update") + public Result update(@RequestBody @Validated(ValidationGroup.edit.class) DeviceQrCodeParam param) { + deviceQrCodeAdminService.update(param); + return Res.ok(); + } + + @PermCode(code = "view", nameCn = "码牌查看", nameEn = "QrCode View") + @Operation(summary = "码牌分页") + @GetMapping("/page") + public Result> page(PageParam pageParam, DeviceQrCodeQuery query) { + return Res.ok(deviceQrCodeAdminService.page(pageParam, query)); + } + + @PermCode(code = "view", nameCn = "码牌查看", nameEn = "QrCode View") + @Operation(summary = "根据id查询码牌") + @GetMapping("/get") + public Result findById(@NotNull(message = "{validation.field.id.notNull}") Long id) { + return Res.ok(deviceQrCodeAdminService.findById(id)); + } + + @PermCode(code = "manage", nameCn = "码牌管理", nameEn = "QrCode Manage") + @Operation(summary = "删除码牌") + @PostMapping("/delete") + public Result delete(@NotNull(message = "{validation.field.id.notNull}") Long id) { + deviceQrCodeAdminService.delete(id); + return Res.ok(); + } + + @PermCode(code = "manage", nameCn = "码牌管理", nameEn = "QrCode Manage") + @Operation(summary = "修改码牌状态") + @PostMapping("/change-status") + public Result changeStatus(@NotNull(message = "{validation.field.id.notNull}") Long id, + @NotBlank(message = "{validation.field.status.notBlank}") String status) { + deviceQrCodeAdminService.changeStatus(id, status); + return Res.ok(); + } +} diff --git a/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/device/qrcode/controller/client/DeviceQrCodeClientController.java b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/device/qrcode/controller/client/DeviceQrCodeClientController.java new file mode 100644 index 000000000..0610551bf --- /dev/null +++ b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/device/qrcode/controller/client/DeviceQrCodeClientController.java @@ -0,0 +1,35 @@ +package cn.daxpay.open.payment.device.qrcode.controller.client; + +import cn.daxpay.open.payment.device.qrcode.result.CodePayInfoResult; +import cn.daxpay.open.payment.device.qrcode.service.CodePayAssistService; +import cn.daxpay.open.platform.core.annotation.IgnoreAuth; +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.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/// # 码牌支付(公开/H5 侧) +/// +/// 供 H5 码牌支付页(/code-pay/:code)扫码后调用, 无需登录态。 +@IgnoreAuth +@Validated +@Tag(name = "码牌支付") +@RestController +@RequestMapping("/client/device/qrcode") +@RequiredArgsConstructor +public class DeviceQrCodeClientController { + + private final CodePayAssistService codePayAssistService; + + @Operation(summary = "根据码牌编码查询支付信息") + @GetMapping("/get-by-code") + public Result getByCode(@NotBlank(message = "{validation.field.code.notBlank}") String code) { + return Res.ok(codePayAssistService.getByCode(code)); + } +} diff --git a/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/device/qrcode/convert/DeviceQrCodeConvert.java b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/device/qrcode/convert/DeviceQrCodeConvert.java new file mode 100644 index 000000000..3837ab333 --- /dev/null +++ b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/device/qrcode/convert/DeviceQrCodeConvert.java @@ -0,0 +1,15 @@ +package cn.daxpay.open.payment.device.qrcode.convert; + +import cn.daxpay.open.payment.device.qrcode.entity.DeviceQrCode; +import cn.daxpay.open.payment.device.qrcode.result.DeviceQrCodeResult; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/// # 支付码牌转换 +/// +@Mapper +public interface DeviceQrCodeConvert { + DeviceQrCodeConvert CONVERT = Mappers.getMapper(DeviceQrCodeConvert.class); + + DeviceQrCodeResult toResult(DeviceQrCode in); +} diff --git a/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/device/qrcode/dao/DeviceQrCodeManager.java b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/device/qrcode/dao/DeviceQrCodeManager.java new file mode 100644 index 000000000..a9502a9bf --- /dev/null +++ b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/device/qrcode/dao/DeviceQrCodeManager.java @@ -0,0 +1,67 @@ +package cn.daxpay.open.payment.device.qrcode.dao; + +import cn.daxpay.open.payment.device.qrcode.entity.DeviceQrCode; +import cn.daxpay.open.payment.device.qrcode.param.DeviceQrCodeQuery; +import cn.daxpay.open.platform.common.mybatisplus.impl.BaseManager; +import cn.daxpay.open.platform.common.mybatisplus.query.generator.QueryGenerator; +import cn.daxpay.open.platform.common.mybatisplus.util.MpUtil; +import cn.daxpay.open.platform.core.rest.param.PageParam; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Repository; + +import java.util.Collection; +import java.util.Optional; + +/// # 支付码牌管理 +/// +@Slf4j +@Repository +@RequiredArgsConstructor +public class DeviceQrCodeManager extends BaseManager { + + /// 根据码牌编码查询 + public Optional findByCode(String code) { + return findByField(DeviceQrCode::getCode, code); + } + + /// 判断码牌编码是否存在(排除指定id, excludeId 为 null 时不排除) + public boolean existsByCode(String code, Long excludeId) { + if (excludeId == null) { + return existedByField(DeviceQrCode::getCode, code); + } + return existedByField(DeviceQrCode::getCode, code, excludeId); + } + + /// 判断批次号是否已存在 + public boolean existedByBatchNo(String batchNo) { + return existedByField(DeviceQrCode::getBatchNo, batchNo); + } + + /// 批量绑定商户与应用 + public void bindMerchant(Collection ids, String mchNo, String appId) { + lambdaUpdate() + .set(DeviceQrCode::getMchNo, mchNo) + .set(DeviceQrCode::getAppId, appId) + .in(DeviceQrCode::getId, ids) + .update(); + } + + /// 批量解绑商户与应用(回空白库存) + public void unbindMerchant(Collection ids) { + lambdaUpdate() + .set(DeviceQrCode::getMchNo, null) + .set(DeviceQrCode::getAppId, null) + .in(DeviceQrCode::getId, ids) + .update(); + } + + /// 分页 + public Page page(PageParam pageParam, DeviceQrCodeQuery query) { + Page mpPage = MpUtil.getMpPage(pageParam); + QueryWrapper wrapper = QueryGenerator.generator(query); + return this.page(mpPage, wrapper); + } +} diff --git a/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/device/qrcode/dao/DeviceQrCodeMapper.java b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/device/qrcode/dao/DeviceQrCodeMapper.java new file mode 100644 index 000000000..19365eaa4 --- /dev/null +++ b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/device/qrcode/dao/DeviceQrCodeMapper.java @@ -0,0 +1,10 @@ +package cn.daxpay.open.payment.device.qrcode.dao; + +import cn.daxpay.open.payment.device.qrcode.entity.DeviceQrCode; +import com.github.yulichang.base.MPJBaseMapper; +import org.apache.ibatis.annotations.Mapper; + +/// # 支付码牌 +@Mapper +public interface DeviceQrCodeMapper extends MPJBaseMapper { +} diff --git a/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/device/qrcode/entity/DeviceQrCode.java b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/device/qrcode/entity/DeviceQrCode.java new file mode 100644 index 000000000..fa6e391fe --- /dev/null +++ b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/device/qrcode/entity/DeviceQrCode.java @@ -0,0 +1,56 @@ +package cn.daxpay.open.payment.device.qrcode.entity; + +import cn.daxpay.open.payment.device.qrcode.convert.DeviceQrCodeConvert; +import cn.daxpay.open.payment.device.qrcode.result.DeviceQrCodeResult; +import cn.daxpay.open.platform.common.mybatisplus.base.MpBaseEntity; +import cn.daxpay.open.platform.common.mybatisplus.function.ToResult; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +/// # 支付码牌 +/// +/// 记录码牌与商户/应用的绑定关系, 扫码后进入 H5 码牌支付页(/code-pay/:code)。 +/// 支持运营端批量创建空白码牌后划拨给商户: mchNo 初始为空, 绑定后写入。 +@EqualsAndHashCode(callSuper = true) +@Data +@Accessors(chain = true) +@TableName("device_qr_code") +public class DeviceQrCode extends MpBaseEntity implements ToResult { + + /// 码牌编码(系统自动生成, 唯一, 二维码 :code 参数) + private String code; + + /// 码牌名称 + private String name; + + /// 批次号(批量创建空白码时写入, 单条新增可空) + private String batchNo; + + /// 所属商户号(划拨后设置, 初始为空表示空白码) + private String mchNo; + + /// 关联应用号(可空, 空白码为空; 绑定后空则使用商户默认应用) + private String appId; + + /// 金额类型: random-自定义金额 / fixed-固定金额 + /// @see cn.daxpay.open.payment.device.enums.QrCodeAmountTypeEnum + private String amountType; + + /// 固定金额(分, amount_type=fixed 时必填) + private Long fixedAmount; + + /// 状态: enabled-启用 / disabled-停用 + /// @see cn.daxpay.open.payment.device.enums.QrCodeStatusEnum + private String status; + + /// 备注 + private String remark; + + /// 转换为返回对象 + @Override + public DeviceQrCodeResult toResult() { + return DeviceQrCodeConvert.CONVERT.toResult(this); + } +} diff --git a/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/device/qrcode/param/DeviceQrCodeBatchParam.java b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/device/qrcode/param/DeviceQrCodeBatchParam.java new file mode 100644 index 000000000..95c5dd62c --- /dev/null +++ b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/device/qrcode/param/DeviceQrCodeBatchParam.java @@ -0,0 +1,55 @@ +package cn.daxpay.open.payment.device.qrcode.param; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; +import lombok.experimental.Accessors; + +/// # 批量创建空白码牌参数 +@Data +@Accessors(chain = true) +@Schema(title = "批量创建空白码牌参数") +public class DeviceQrCodeBatchParam { + + /// 批次号(全局唯一, 同时作为码牌编码前缀) + @Schema(description = "批次号") + @NotBlank(message = "{validation.field.batchNo.notBlank}") + @Size(max = 64, message = "{validation.field.batchNo.size}") + private String batchNo; + + /// 创建数量(1-999) + @Schema(description = "创建数量") + @NotNull(message = "{validation.field.count.notNull}") + @Min(value = 1, message = "{validation.field.count.min}") + @Max(value = 999, message = "{validation.field.count.max}") + private Integer count; + + /// 码牌名称(可选, 整批统一名称) + @Schema(description = "码牌名称") + @Size(max = 100, message = "{validation.field.qrCodeName.size}") + private String name; + + /// 金额类型: random-自定义金额 / fixed-固定金额 + /// @see cn.daxpay.open.payment.device.enums.QrCodeAmountTypeEnum + @Schema(description = "金额类型(random-自定义/fixed-固定)") + @NotBlank(message = "{validation.field.amountType.notBlank}") + private String amountType; + + /// 固定金额(分, amount_type=fixed 时必填, 由 Service 层校验) + @Schema(description = "固定金额(分)") + private Long fixedAmount; + + /// 状态(enabled/disabled), 空则默认启用 + /// @see cn.daxpay.open.payment.device.enums.QrCodeStatusEnum + @Schema(description = "状态(enabled-启用/disabled-停用), 空则默认启用") + private String status; + + /// 备注 + @Schema(description = "备注") + @Size(max = 500, message = "{validation.field.remark.size}") + private String remark; +} diff --git a/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/device/qrcode/param/DeviceQrCodeBindMerchantParam.java b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/device/qrcode/param/DeviceQrCodeBindMerchantParam.java new file mode 100644 index 000000000..0e1c7090b --- /dev/null +++ b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/device/qrcode/param/DeviceQrCodeBindMerchantParam.java @@ -0,0 +1,32 @@ +package cn.daxpay.open.payment.device.qrcode.param; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.Size; +import lombok.Data; +import lombok.experimental.Accessors; + +import java.util.List; + +/// # 码牌绑定商户参数 +@Data +@Accessors(chain = true) +@Schema(title = "码牌绑定商户参数") +public class DeviceQrCodeBindMerchantParam { + + /// 码牌主键列表 + @Schema(description = "码牌主键列表") + @NotEmpty(message = "{validation.field.ids.notEmpty}") + private List ids; + + /// 商户号 + @Schema(description = "商户号") + @NotBlank(message = "{validation.field.mchNo.notBlank}") + private String mchNo; + + /// 关联应用号(可空, 空则使用商户默认应用) + @Schema(description = "关联应用号") + @Size(max = 50, message = "{validation.field.appId.size}") + private String appId; +} diff --git a/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/device/qrcode/param/DeviceQrCodeParam.java b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/device/qrcode/param/DeviceQrCodeParam.java new file mode 100644 index 000000000..a7c2b454f --- /dev/null +++ b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/device/qrcode/param/DeviceQrCodeParam.java @@ -0,0 +1,57 @@ +package cn.daxpay.open.payment.device.qrcode.param; + +import cn.daxpay.open.platform.core.validation.ValidationGroup; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; +import lombok.experimental.Accessors; + +/// # 支付码牌 +@Data +@Accessors(chain = true) +@Schema(title = "支付码牌") +public class DeviceQrCodeParam { + + /// 主键 + @Schema(description = "主键") + @NotNull(message = "{validation.field.id.notNull}", groups = ValidationGroup.edit.class) + private Long id; + + /// 码牌名称 + @Schema(description = "码牌名称") + @NotBlank(message = "{validation.field.qrCodeName.notBlank}") + @Size(max = 100, message = "{validation.field.qrCodeName.size}") + private String name; + + /// 商户号(新增快捷绑定时必填; 编辑不改归属, 仅 add 校验) + @Schema(description = "商户号") + @NotBlank(message = "{validation.field.mchNo.notBlank}", groups = ValidationGroup.add.class) + private String mchNo; + + /// 关联应用号(可空, 空则使用商户默认应用) + @Schema(description = "关联应用号") + @Size(max = 50, message = "{validation.field.appId.size}") + private String appId; + + /// 金额类型: random-自定义金额 / fixed-固定金额 + /// @see cn.daxpay.open.payment.device.enums.QrCodeAmountTypeEnum + @Schema(description = "金额类型(random-自定义/fixed-固定)") + @NotBlank(message = "{validation.field.amountType.notBlank}") + private String amountType; + + /// 固定金额(分, amount_type=fixed 时必填, 由 Service 层校验) + @Schema(description = "固定金额(分)") + private Long fixedAmount; + + /// 状态(enabled/disabled) + /// @see cn.daxpay.open.payment.device.enums.QrCodeStatusEnum + @Schema(description = "状态") + private String status; + + /// 备注 + @Schema(description = "备注") + @Size(max = 500, message = "{validation.field.remark.size}") + private String remark; +} diff --git a/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/device/qrcode/param/DeviceQrCodeQuery.java b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/device/qrcode/param/DeviceQrCodeQuery.java new file mode 100644 index 000000000..aed534a61 --- /dev/null +++ b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/device/qrcode/param/DeviceQrCodeQuery.java @@ -0,0 +1,45 @@ +package cn.daxpay.open.payment.device.qrcode.param; + +import cn.daxpay.open.platform.core.annotation.QueryParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.experimental.Accessors; + +/// # 支付码牌查询参数 +@Data +@QueryParam(type = QueryParam.CompareTypeEnum.LIKE) +@Accessors(chain = true) +@Schema(title = "支付码牌查询参数") +public class DeviceQrCodeQuery { + + /// 码牌编码 + @Schema(description = "码牌编码") + @QueryParam(type = QueryParam.CompareTypeEnum.LIKE) + private String code; + + /// 码牌名称 + @Schema(description = "码牌名称") + private String name; + + /// 批次号 + @Schema(description = "批次号") + @QueryParam(type = QueryParam.CompareTypeEnum.EQ) + private String batchNo; + + /// 商户号 + @Schema(description = "商户号") + @QueryParam(type = QueryParam.CompareTypeEnum.EQ) + private String mchNo; + + /// 金额类型 + /// @see cn.daxpay.open.payment.device.enums.QrCodeAmountTypeEnum + @Schema(description = "金额类型") + @QueryParam(type = QueryParam.CompareTypeEnum.EQ) + private String amountType; + + /// 状态 + /// @see cn.daxpay.open.payment.device.enums.QrCodeStatusEnum + @Schema(description = "状态") + @QueryParam(type = QueryParam.CompareTypeEnum.EQ) + private String status; +} diff --git a/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/device/qrcode/result/CodePayInfoResult.java b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/device/qrcode/result/CodePayInfoResult.java new file mode 100644 index 000000000..293407329 --- /dev/null +++ b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/device/qrcode/result/CodePayInfoResult.java @@ -0,0 +1,28 @@ +package cn.daxpay.open.payment.device.qrcode.result; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.experimental.Accessors; + +/// # 码牌支付信息(公开接口返回, 脱敏) +/// +/// 供 H5 码牌支付页(/code-pay/:code)扫码后查询展示, 不含 appId/mchNo 等内部字段。 +@Data +@Accessors(chain = true) +@Schema(title = "码牌支付信息") +public class CodePayInfoResult { + + @Schema(description = "码牌编码") + private String code; + + @Schema(description = "码牌名称") + private String name; + + /// 金额类型 + /// @see cn.daxpay.open.payment.device.enums.QrCodeAmountTypeEnum + @Schema(description = "金额类型(random-自定义/fixed-固定)") + private String amountType; + + @Schema(description = "固定金额(分)") + private Long fixedAmount; +} diff --git a/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/device/qrcode/result/DeviceQrCodeResult.java b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/device/qrcode/result/DeviceQrCodeResult.java new file mode 100644 index 000000000..ba09d32e1 --- /dev/null +++ b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/device/qrcode/result/DeviceQrCodeResult.java @@ -0,0 +1,53 @@ +package cn.daxpay.open.payment.device.qrcode.result; + +import cn.daxpay.open.payment.merchant.entity.info.MerchantInfo; +import cn.daxpay.open.platform.core.annotation.Trans; +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 DeviceQrCodeResult extends BaseResult { + + @Schema(description = "码牌编码") + private String code; + + @Schema(description = "码牌名称") + private String name; + + @Schema(description = "批次号") + private String batchNo; + + @Schema(description = "所属商户号(空表示未绑定)") + private String mchNo; + + /// 商户名称(由 mchNo 翻译, 未绑定为空) + @Trans(entity = MerchantInfo.class, source = "mchNo", result = "mchName") + @Schema(description = "商户名称") + private String mchName; + + @Schema(description = "关联应用号(空表示使用商户默认应用)") + private String appId; + + /// 金额类型 + /// @see cn.daxpay.open.payment.device.enums.QrCodeAmountTypeEnum + @Schema(description = "金额类型(random-自定义/fixed-固定)") + private String amountType; + + @Schema(description = "固定金额(分)") + private Long fixedAmount; + + /// 状态 + /// @see cn.daxpay.open.payment.device.enums.QrCodeStatusEnum + @Schema(description = "状态(enabled-启用/disabled-停用)") + private String status; + + @Schema(description = "备注") + private String remark; +} diff --git a/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/device/qrcode/service/CodePayAssistService.java b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/device/qrcode/service/CodePayAssistService.java new file mode 100644 index 000000000..8a89e856e --- /dev/null +++ b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/device/qrcode/service/CodePayAssistService.java @@ -0,0 +1,56 @@ +package cn.daxpay.open.payment.device.qrcode.service; + +import cn.daxpay.open.payment.device.enums.QrCodeStatusEnum; +import cn.daxpay.open.payment.device.qrcode.dao.DeviceQrCodeManager; +import cn.daxpay.open.payment.device.qrcode.entity.DeviceQrCode; +import cn.daxpay.open.payment.device.qrcode.result.CodePayInfoResult; +import cn.daxpay.open.payment.merchant.dto.MerchantAccessInfo; +import cn.daxpay.open.payment.merchant.service.query.MerchantAccessQueryService; +import cn.daxpay.open.platform.core.exception.DataNotExistException; +import cn.daxpay.open.platform.core.exception.operation.OperationFailException; +import cn.daxpay.open.platform.core.code.CommonCode; +import cn.hutool.core.util.StrUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/// # 码牌支付编排(公开/H5 侧) +/// +/// 一期: 仅提供按编码查询码牌信息(供 H5 扫码展示)。 +/// 二期: 扩展 OAuth 重定向获取 openId 与发起 JSAPI 支付(复用 ChannelAuthService/NormalPayService)。 +@Slf4j +@Service +@RequiredArgsConstructor +public class CodePayAssistService { + + private final DeviceQrCodeManager deviceQrCodeManager; + private final MerchantAccessQueryService merchantAccessQueryService; + + /// 根据码牌编码查询支付信息(公开接口, 脱敏返回) + /// + /// 校验码牌存在、启用、已分配商户, 且商户存在。 + public CodePayInfoResult getByCode(String code) { + DeviceQrCode entity = deviceQrCodeManager.findByCode(code) + // 码牌: 码牌不存在 + .orElseThrow(() -> new DataNotExistException("error.device.qrcode.notFound")); + // 码牌停用 + if (!QrCodeStatusEnum.ENABLED.getCode().equals(entity.getStatus())) { + throw new OperationFailException(CommonCode.FAIL_CODE, "error.device.qrcode.disabled"); + } + // 空白码未划拨商户 + if (StrUtil.isBlank(entity.getMchNo())) { + throw new OperationFailException(CommonCode.FAIL_CODE, "error.device.qrcode.notAssigned"); + } + // 查商户并校验存在(启用校验留待二期支付链路 NormalPayService 统一处理) + MerchantAccessInfo merchant = merchantAccessQueryService.getMerchantByMchNo(entity.getMchNo()); + if (merchant == null) { + // 码牌: 商户不存在 + throw new DataNotExistException("error.device.qrcode.mchNotFound"); + } + return new CodePayInfoResult() + .setCode(entity.getCode()) + .setName(entity.getName()) + .setAmountType(entity.getAmountType()) + .setFixedAmount(entity.getFixedAmount()); + } +} diff --git a/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/device/qrcode/service/DeviceQrCodeAdminService.java b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/device/qrcode/service/DeviceQrCodeAdminService.java new file mode 100644 index 000000000..d87547dbf --- /dev/null +++ b/daxpay-payment/daxpay-payment-common/src/main/java/cn/daxpay/open/payment/device/qrcode/service/DeviceQrCodeAdminService.java @@ -0,0 +1,194 @@ +package cn.daxpay.open.payment.device.qrcode.service; + +import cn.daxpay.open.payment.common.service.MerchantContextLoader; +import cn.daxpay.open.payment.device.enums.QrCodeAmountTypeEnum; +import cn.daxpay.open.payment.device.enums.QrCodeStatusEnum; +import cn.daxpay.open.payment.device.qrcode.dao.DeviceQrCodeManager; +import cn.daxpay.open.payment.device.qrcode.entity.DeviceQrCode; +import cn.daxpay.open.payment.device.qrcode.param.DeviceQrCodeBatchParam; +import cn.daxpay.open.payment.device.qrcode.param.DeviceQrCodeBindMerchantParam; +import cn.daxpay.open.payment.device.qrcode.param.DeviceQrCodeParam; +import cn.daxpay.open.payment.device.qrcode.param.DeviceQrCodeQuery; +import cn.daxpay.open.payment.device.qrcode.result.DeviceQrCodeResult; +import cn.daxpay.open.payment.merchant.dto.MchAppInfoAccessInfo; +import cn.daxpay.open.platform.common.mybatisplus.util.MpUtil; +import cn.daxpay.open.platform.common.translate.service.TransService; +import cn.daxpay.open.platform.core.code.CommonCode; +import cn.daxpay.open.platform.core.exception.DataNotExistException; +import cn.daxpay.open.platform.core.exception.operation.OperationFailException; +import cn.daxpay.open.platform.core.rest.param.PageParam; +import cn.daxpay.open.platform.core.rest.result.PageResult; +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.StrUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.stream.IntStream; + +/// # 支付码牌管理(运营端) +/// +/// 维护码牌台账, 支持批量创建空白码与划拨绑定商户。 +/// 扫码后的支付编排由 [CodePayAssistService] 承担。 +@Slf4j +@Service +@RequiredArgsConstructor +public class DeviceQrCodeAdminService { + + /// 码牌编码前缀(单条快捷新增) + private static final String CODE_PREFIX = "QR"; + + private final DeviceQrCodeManager deviceQrCodeManager; + private final MerchantContextLoader merchantContextLoader; + private final TransService transService; + + /// 新增码牌(快捷路径: 直接绑定商户, 自动生成编码) + @Transactional(rollbackFor = Exception.class) + public void add(DeviceQrCodeParam param) { + // 金额类型校验 + QrCodeAmountTypeEnum amountType = QrCodeAmountTypeEnum.findByCode(param.getAmountType()); + // 固定金额: fixed 类型必填且大于 0 + validateFixedAmount(amountType, param.getFixedAmount()); + // 应用解析: appId 空则取商户默认应用, 否则校验启用与归属, 回填 appId + String appId = resolveAppId(param.getMchNo(), param.getAppId()); + DeviceQrCode entity = new DeviceQrCode() + .setCode(generateCode()) + .setName(param.getName()) + .setMchNo(param.getMchNo()) + .setAppId(appId) + .setAmountType(amountType.getCode()) + .setFixedAmount(amountType == QrCodeAmountTypeEnum.FIXED ? param.getFixedAmount() : null) + .setStatus(QrCodeStatusEnum.ENABLED.getCode()) + .setRemark(param.getRemark()); + deviceQrCodeManager.save(entity); + } + + /// 批量创建空白码牌(不绑商户, 进入平台库存) + @Transactional(rollbackFor = Exception.class) + public void createBatch(DeviceQrCodeBatchParam param) { + // 批次号唯一 + if (deviceQrCodeManager.existedByBatchNo(param.getBatchNo())) { + // 码牌: 批次号已存在 + throw new OperationFailException(CommonCode.FAIL_CODE, "error.device.qrcode.batchNoExists"); + } + // 金额类型校验 + QrCodeAmountTypeEnum amountType = QrCodeAmountTypeEnum.findByCode(param.getAmountType()); + validateFixedAmount(amountType, param.getFixedAmount()); + Long fixedAmount = amountType == QrCodeAmountTypeEnum.FIXED ? param.getFixedAmount() : null; + // 状态: 空则默认启用 + String status = StrUtil.isBlank(param.getStatus()) + ? QrCodeStatusEnum.ENABLED.getCode() + : QrCodeStatusEnum.findByCode(param.getStatus()).getCode(); + // 编码: 批次号 + 三位序号(001-999) + List list = IntStream.rangeClosed(1, param.getCount()) + .mapToObj(i -> new DeviceQrCode() + .setCode(param.getBatchNo() + String.format("%03d", i)) + .setName(param.getName()) + .setBatchNo(param.getBatchNo()) + .setAmountType(amountType.getCode()) + .setFixedAmount(fixedAmount) + .setStatus(status) + .setRemark(param.getRemark())) + .toList(); + deviceQrCodeManager.saveAll(list); + } + + /// 判断批次号是否已存在 + public boolean existsByBatchNo(String batchNo) { + return deviceQrCodeManager.existedByBatchNo(batchNo); + } + + /// 批量绑定商户(可覆盖已绑定归属) + @Transactional(rollbackFor = Exception.class) + public void bindMerchant(DeviceQrCodeBindMerchantParam param) { + // 应用解析: appId 空则取商户默认应用 + String appId = resolveAppId(param.getMchNo(), param.getAppId()); + deviceQrCodeManager.bindMerchant(param.getIds(), param.getMchNo(), appId); + } + + /// 批量解绑商户(回空白库存, 保留编码/批次/金额配置) + @Transactional(rollbackFor = Exception.class) + public void unbindMerchant(List ids) { + if (ids == null || ids.isEmpty()) { + // 码牌: 请选择码牌 + throw new OperationFailException(CommonCode.FAIL_CODE, "error.device.qrcode.idsEmpty"); + } + deviceQrCodeManager.unbindMerchant(ids); + } + + /// 修改码牌(编码/归属不可改; 归属走 bind/unbind) + @Transactional(rollbackFor = Exception.class) + public void update(DeviceQrCodeParam param) { + DeviceQrCode entity = deviceQrCodeManager.findById(param.getId()) + // 码牌: 码牌不存在 + .orElseThrow(() -> new DataNotExistException("error.device.qrcode.notFound")); + // 金额类型校验 + QrCodeAmountTypeEnum amountType = QrCodeAmountTypeEnum.findByCode(param.getAmountType()); + validateFixedAmount(amountType, param.getFixedAmount()); + // 仅更新业务配置, 不改 mchNo/appId + entity.setName(param.getName()) + .setAmountType(amountType.getCode()) + .setFixedAmount(amountType == QrCodeAmountTypeEnum.FIXED ? param.getFixedAmount() : null) + .setRemark(param.getRemark()); + deviceQrCodeManager.updateById(entity); + } + + /// 分页 + public PageResult page(PageParam pageParam, DeviceQrCodeQuery query) { + PageResult pageResult = MpUtil.toPageResult(deviceQrCodeManager.page(pageParam, query)); + // 翻译商户名称(mchNo -> mchName) + transService.translate(pageResult); + return pageResult; + } + + /// 根据id查询 + public DeviceQrCodeResult findById(Long id) { + DeviceQrCodeResult result = deviceQrCodeManager.findById(id) + // 码牌: 码牌不存在 + .orElseThrow(() -> new DataNotExistException("error.device.qrcode.notFound")) + .toResult(); + // 翻译商户名称 + transService.translate(result); + return result; + } + + /// 删除 + public void delete(Long id) { + deviceQrCodeManager.findById(id) + .orElseThrow(() -> new DataNotExistException("error.device.qrcode.notFound")); + deviceQrCodeManager.deleteById(id); + } + + /// 修改状态(启用/停用) + @Transactional(rollbackFor = Exception.class) + public void changeStatus(Long id, String status) { + QrCodeStatusEnum statusEnum = QrCodeStatusEnum.findByCode(status); + DeviceQrCode entity = deviceQrCodeManager.findById(id) + .orElseThrow(() -> new DataNotExistException("error.device.qrcode.notFound")); + entity.setStatus(statusEnum.getCode()); + deviceQrCodeManager.updateById(entity); + } + + /// 生成码牌编码: 前缀 + 雪花ID + private String generateCode() { + return CODE_PREFIX + IdUtil.getSnowflakeNextId(); + } + + /// 应用解析: 复用 [MerchantContextLoader.resolveApp], appId 空取商户默认应用, 返回解析后的 appId + private String resolveAppId(String mchNo, String appId) { + MchAppInfoAccessInfo mchApp = merchantContextLoader.resolveApp(mchNo, appId); + return mchApp.getAppId(); + } + + /// 固定金额校验: fixed 类型必填且大于 0 + private void validateFixedAmount(QrCodeAmountTypeEnum amountType, Long fixedAmount) { + if (amountType == QrCodeAmountTypeEnum.FIXED) { + if (fixedAmount == null || fixedAmount <= 0) { + // 码牌: 固定金额必须大于 0 + throw new OperationFailException(CommonCode.FAIL_CODE, "error.device.qrcode.fixedAmountInvalid"); + } + } + } +} diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/enum/qrcode_amount_type.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/enum/qrcode_amount_type.json new file mode 100644 index 000000000..d3b3f327b --- /dev/null +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/enum/qrcode_amount_type.json @@ -0,0 +1,4 @@ +{ + "random": "Custom Amount", + "fixed": "Fixed Amount" +} diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/enum/qrcode_status.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/enum/qrcode_status.json new file mode 100644 index 000000000..bbcc3518e --- /dev/null +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/enum/qrcode_status.json @@ -0,0 +1,4 @@ +{ + "enabled": "Enabled", + "disabled": "Disabled" +} diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/error/device/qrcode.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/error/device/qrcode.json new file mode 100644 index 000000000..387a5eb21 --- /dev/null +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/error/device/qrcode.json @@ -0,0 +1,11 @@ +{ + "notFound": "QR code board does not exist", + "disabled": "QR code board is disabled", + "mchNotFound": "The merchant of the QR code board does not exist", + "amountTypeNotFound": "No matching QR code amount type found: {0}", + "statusNotFound": "No matching QR code status found: {0}", + "fixedAmountInvalid": "Fixed amount must be greater than 0", + "batchNoExists": "Batch number already exists", + "notAssigned": "QR code board has not been assigned to a merchant", + "idsEmpty": "Please select QR code boards" +} 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 2aa79e8dc..464a456a3 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 @@ -15,6 +15,9 @@ "clientSecret": { "notBlank": "Client secret cannot be blank" }, + "clientKey": { + "notBlank": "Client Key cannot be blank" + }, "clientCode": { "notBlank": "Client code cannot be blank" }, @@ -552,6 +555,25 @@ "vendorConfigId": { "notNull": "Vendor config cannot be null" }, + "qrCodeName": { + "notBlank": "QR code name cannot be blank", + "size": "QR code name cannot exceed 100 characters" + }, + "amountType": { + "notBlank": "Amount type cannot be blank" + }, + "batchNo": { + "notBlank": "Batch number cannot be blank", + "size": "Batch number cannot exceed 64 characters" + }, + "count": { + "notNull": "Count cannot be null", + "min": "Count cannot be less than 1", + "max": "Count cannot be greater than 999" + }, + "ids": { + "notEmpty": "Please select QR code boards" + }, "configName": { "notBlank": "Config name cannot be blank" }, diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/enum/qrcode_amount_type.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/enum/qrcode_amount_type.json new file mode 100644 index 000000000..613e5db3c --- /dev/null +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/enum/qrcode_amount_type.json @@ -0,0 +1,4 @@ +{ + "random": "自定义金额", + "fixed": "固定金额" +} diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/enum/qrcode_status.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/enum/qrcode_status.json new file mode 100644 index 000000000..5b25718a6 --- /dev/null +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/enum/qrcode_status.json @@ -0,0 +1,4 @@ +{ + "enabled": "启用", + "disabled": "停用" +} diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/error/device/qrcode.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/error/device/qrcode.json new file mode 100644 index 000000000..ccb577803 --- /dev/null +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/error/device/qrcode.json @@ -0,0 +1,11 @@ +{ + "notFound": "码牌不存在", + "disabled": "码牌已停用", + "mchNotFound": "码牌所属商户不存在", + "amountTypeNotFound": "未找到对应的码牌金额类型: {0}", + "statusNotFound": "未找到对应的码牌状态: {0}", + "fixedAmountInvalid": "固定金额必须大于0", + "batchNoExists": "批次号已存在", + "notAssigned": "码牌未分配商户", + "idsEmpty": "请选择码牌" +} 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 1bc3198f0..ba69c4ecc 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 @@ -15,6 +15,9 @@ "clientSecret": { "notBlank": "客户端密钥不能为空" }, + "clientKey": { + "notBlank": "Client Key不能为空" + }, "clientCode": { "notBlank": "终端不能为空" }, @@ -555,6 +558,25 @@ "vendorConfigId": { "notNull": "厂商配置不能为空" }, + "qrCodeName": { + "notBlank": "码牌名称不能为空", + "size": "码牌名称不可超过100位" + }, + "amountType": { + "notBlank": "金额类型不能为空" + }, + "batchNo": { + "notBlank": "批次号不能为空", + "size": "批次号不可超过64位" + }, + "count": { + "notNull": "创建数量不能为空", + "min": "创建数量不能小于1", + "max": "创建数量不能大于999" + }, + "ids": { + "notEmpty": "请选择码牌" + }, "configName": { "notBlank": "配置名称不能为空" },