zhangchen 1 месяц назад
Родитель
Сommit
e5b6e2a487

+ 153 - 0
alien-entity/src/main/java/shop/alien/entity/store/StorePaymentConfig.java

@@ -0,0 +1,153 @@
+package shop.alien.entity.store;
+
+import com.baomidou.mybatisplus.annotation.*;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import org.springframework.format.annotation.DateTimeFormat;
+import shop.alien.util.typehandler.BlobByteArrayTypeHandler;
+
+import java.util.Date;
+
+/**
+ * 支付配置表(支付宝等)
+ *
+ * @author system
+ */
+@Data
+@JsonInclude
+@TableName("store_payment_config")
+@ApiModel(value = "StorePaymentConfig对象", description = "支付配置表")
+public class StorePaymentConfig {
+
+    @ApiModelProperty(value = "主键")
+    @TableId(value = "id", type = IdType.AUTO)
+    private Integer id;
+
+    @ApiModelProperty(value = "店铺ID")
+    @TableField("store_id")
+    private Integer storeId;
+
+    @ApiModelProperty(value = "店铺用户id")
+    @TableField("store_user_id")
+    private Integer storeUserId;
+
+    @ApiModelProperty(value = "应用ID")
+    @TableField("app_id")
+    private String appId;
+
+    @ApiModelProperty(value = "应用私钥")
+    @TableField("app_secret_cert")
+    private String appSecretCert;
+
+    @ApiModelProperty(value = "应用公钥证书文件(存储文件内容)(支付宝)BLOB,原样存取不经过字符编码")
+    @TableField(value = "app_public_cert", typeHandler = BlobByteArrayTypeHandler.class)
+    private byte[] appPublicCert;
+
+    @ApiModelProperty(value = "应用公钥证书路径")
+    @TableField("app_public_cert_path")
+    private String appPublicCertPath;
+
+    @ApiModelProperty(value = "应用公钥证书文件名称")
+    @TableField("app_public_cert_name")
+    private String appPublicCertName;
+
+    @ApiModelProperty(value = "支付宝公钥证书文件(存储文件内容)(支付宝)BLOB,原样存取不经过字符编码")
+    @TableField(value = "alipay_public_cert", typeHandler = BlobByteArrayTypeHandler.class)
+    private byte[] alipayPublicCert;
+
+    @ApiModelProperty(value = "支付宝公钥证书路径")
+    @TableField("alipay_public_cert_path")
+    private String alipayPublicCertPath;
+
+    @ApiModelProperty(value = "支付宝公钥证书文件名称")
+    @TableField("alipay_public_cert_name")
+    private String alipayPublicCertName;
+
+    @ApiModelProperty(value = "支付宝根证书文件(存储文件内容)(支付宝)BLOB,原样存取不经过字符编码")
+    @TableField(value = "alipay_root_cert", typeHandler = BlobByteArrayTypeHandler.class)
+    private byte[] alipayRootCert;
+
+    @ApiModelProperty(value = "支付宝根证书路径")
+    @TableField("alipay_root_cert_path")
+    private String alipayRootCertPath;
+
+    @ApiModelProperty(value = "支付宝根证书文件名称")
+    @TableField("alipay_root_cert_name")
+    private String alipayRootCertName;
+
+    @ApiModelProperty(value = "微信appId")
+    @TableField("wechat_app_id")
+    private String wechatAppId;
+
+    @ApiModelProperty(value = "微信支付mini appId")
+    @TableField("wechat_mini_app_id")
+    private String wechatMiniAppId;
+
+    @ApiModelProperty(value = "微信mchId")
+    @TableField("wechat_mch_id")
+    private String wechatMchId;
+
+    @ApiModelProperty(value = "微信商户API证书序列号")
+    @TableField("merchant_serial_number")
+    private String merchantSerialNumber;
+
+    @ApiModelProperty(value = "微信商户的 APIv3 Key")
+    @TableField("api_v3_key")
+    private String apiV3Key;
+
+    @ApiModelProperty(value = "微信支付公钥ID")
+    @TableField("wechat_pay_public_key_id")
+    private String wechatPayPublicKeyId;
+
+    @ApiModelProperty(value = "微信私钥路径")
+    @TableField("wechat_private_key_path")
+    private String wechatPrivateKeyPath;
+
+    @ApiModelProperty(value = "微信私钥文件名")
+    @TableField("wechat_private_key_name")
+    private String wechatPrivateKeyName;
+
+    @ApiModelProperty(value = "微信私钥文件(存储文件内容)BLOB,原样存取不经过字符编码")
+    @TableField(value = "wechat_private_key_file", typeHandler = BlobByteArrayTypeHandler.class)
+    private byte[] wechatPrivateKeyFile;
+
+    @ApiModelProperty(value = "微信公钥路径")
+    @TableField("wechat_pay_public_key_file_path")
+    private String wechatPayPublicKeyFilePath;
+
+    @ApiModelProperty(value = "微信公钥文件名")
+    @TableField("wechat_pay_public_key_file_name")
+    private String wechatPayPublicKeyFileName;
+
+    @ApiModelProperty(value = "微信公钥文件(存储文件内容)BLOB,原样存取不经过字符编码")
+    @TableField(value = "wechat_pay_public_key_file", typeHandler = BlobByteArrayTypeHandler.class)
+    private byte[] wechatPayPublicKeyFile;
+
+    @ApiModelProperty(value = "删除标记, 0:未删除, 1:已删除")
+    @TableField("delete_flag")
+    @TableLogic
+    private Integer deleteFlag;
+
+    @ApiModelProperty(value = "创建时间")
+    @TableField(value = "created_time", fill = FieldFill.INSERT)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date createdTime;
+
+    @ApiModelProperty(value = "创建人ID")
+    @TableField("created_user_id")
+    private Integer createdUserId;
+
+    @ApiModelProperty(value = "修改时间")
+    @TableField(value = "updated_time", fill = FieldFill.INSERT_UPDATE)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date updatedTime;
+
+    @ApiModelProperty(value = "修改人ID")
+    @TableField("updated_user_id")
+    private Integer updatedUserId;
+}

+ 15 - 0
alien-entity/src/main/java/shop/alien/mapper/StorePaymentConfigMapper.java

@@ -0,0 +1,15 @@
+package shop.alien.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+import shop.alien.entity.store.StorePaymentConfig;
+
+/**
+ * 支付配置表 Mapper 接口
+ *
+ * @author system
+ */
+@Mapper
+public interface StorePaymentConfigMapper extends BaseMapper<StorePaymentConfig> {
+
+}

+ 599 - 0
alien-store/src/main/java/shop/alien/store/controller/StorePaymentConfigController.java

@@ -0,0 +1,599 @@
+package shop.alien.store.controller;
+
+import com.alibaba.fastjson.JSONObject;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import io.swagger.annotations.*;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.StorePaymentConfig;
+import shop.alien.entity.store.StoreUser;
+import shop.alien.store.service.StorePaymentConfigService;
+import shop.alien.store.service.StoreUserService;
+import shop.alien.util.common.JwtUtil;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 支付配置 前端控制器
+ *
+ * @author system
+ */
+@Slf4j
+@Api(tags = {"支付配置"})
+@ApiSort(18)
+@CrossOrigin
+@RestController
+@RequestMapping("/store/payment/config")
+@RequiredArgsConstructor
+public class StorePaymentConfigController {
+
+    private final StorePaymentConfigService storePaymentConfigService;
+    private final StoreUserService storeUserService;
+
+    @ApiOperationSupport(order = 1)
+    @ApiOperation("新增支付配置(JSON  body)")
+    @PostMapping("/add")
+    public R<Boolean> add(@RequestBody StorePaymentConfig config) {
+        log.info("StorePaymentConfigController.add?config.storeId={}, appId={}", config.getStoreId(), config.getAppId());
+        if (config.getStoreId() == null) {
+            return R.fail("店铺ID不能为空");
+        }
+        if (config.getAppId() == null || config.getAppId().trim().isEmpty()) {
+            return R.fail("应用ID不能为空");
+        }
+        try {
+            fillCreatedUser(config);
+            boolean ok = storePaymentConfigService.save(config);
+            return ok ? R.success("新增成功") : R.fail("新增失败");
+        } catch (Exception e) {
+            log.error("新增支付配置失败", e);
+            return R.fail("新增失败:" + e.getMessage());
+        }
+    }
+
+    @ApiOperationSupport(order = 2)
+    @ApiOperation("修改支付配置(JSON body)")
+    @PostMapping("/update")
+    public R<Boolean> update(@RequestBody StorePaymentConfig config) {
+        log.info("StorePaymentConfigController.update?id={}", config.getId());
+        if (config.getId() == null) {
+            return R.fail("配置ID不能为空");
+        }
+        try {
+            fillUpdatedUser(config);
+            boolean ok = storePaymentConfigService.updateById(config);
+            return ok ? R.success("修改成功") : R.fail("修改失败");
+        } catch (Exception e) {
+            log.error("修改支付配置失败", e);
+            return R.fail("修改失败:" + e.getMessage());
+        }
+    }
+
+    @ApiOperationSupport(order = 2)
+    @ApiOperation(value = "新增支付配置(带三个证书文件,证书内容直接存入表 store_payment_config)", notes = "multipart:storeId、appId 必填;appSecretCert、*CertPath 选填;三个文件:appPublicCert、alipayPublicCert、alipayRootCert(.crt/.pem/.cer)")
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "storeId", value = "店铺ID", dataType = "int", paramType = "form", required = true),
+            @ApiImplicitParam(name = "appId", value = "应用ID", dataType = "string", paramType = "form", required = true),
+            @ApiImplicitParam(name = "appSecretCert", value = "应用私钥", dataType = "string", paramType = "form"),
+            @ApiImplicitParam(name = "appPublicCert", value = "应用公钥证书文件", dataType = "file", paramType = "form"),
+            @ApiImplicitParam(name = "alipayPublicCert", value = "支付宝公钥证书文件", dataType = "file", paramType = "form"),
+            @ApiImplicitParam(name = "alipayRootCert", value = "支付宝根证书文件", dataType = "file", paramType = "form")
+    })
+    @PostMapping("/addWithCerts")
+    public R<Boolean> addWithCerts(
+            @RequestParam Integer storeId,
+            @RequestParam String appId,
+            @RequestParam(required = false) String appSecretCert,
+            @RequestParam(required = false) String appPublicCertPath,
+            @RequestParam(required = false) String alipayPublicCertPath,
+            @RequestParam(required = false) String alipayRootCertPath,
+            @RequestParam(value = "appPublicCert", required = false) MultipartFile appPublicCertFile,
+            @RequestParam(value = "alipayPublicCert", required = false) MultipartFile alipayPublicCertFile,
+            @RequestParam(value = "alipayRootCert", required = false) MultipartFile alipayRootCertFile) {
+        log.info("StorePaymentConfigController.addWithCerts storeId={}, appId={}", storeId, appId);
+        if (storeId == null) {
+            return R.fail("店铺ID不能为空");
+        }
+        if (appId == null || appId.trim().isEmpty()) {
+            return R.fail("应用ID不能为空");
+        }
+        try {
+            StorePaymentConfig config = new StorePaymentConfig();
+            config.setStoreId(storeId);
+            config.setAppId(appId.trim());
+            config.setAppSecretCert(appSecretCert);
+            config.setAppPublicCertPath(appPublicCertPath);
+            config.setAlipayPublicCertPath(alipayPublicCertPath);
+            config.setAlipayRootCertPath(alipayRootCertPath);
+            fillCertContent(config, appPublicCertFile, alipayPublicCertFile, alipayRootCertFile);
+            fillCreatedUser(config);
+            boolean ok = storePaymentConfigService.save(config);
+            return ok ? R.success("新增成功") : R.fail("新增失败");
+        } catch (Exception e) {
+            log.error("新增支付配置(带证书)失败", e);
+            return R.fail("新增失败:" + e.getMessage());
+        }
+    }
+
+    @ApiOperationSupport(order = 3)
+    @ApiOperation(value = "为已有配置上传/覆盖三个证书文件(内容存入表)", notes = "multipart:id 必填;三个文件:appPublicCert、alipayPublicCert、alipayRootCert,传哪个更新哪个")
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "id", value = "配置ID", dataType = "int", paramType = "form", required = true),
+            @ApiImplicitParam(name = "appPublicCert", value = "应用公钥证书文件", dataType = "file", paramType = "form"),
+            @ApiImplicitParam(name = "alipayPublicCert", value = "支付宝公钥证书文件", dataType = "file", paramType = "form"),
+            @ApiImplicitParam(name = "alipayRootCert", value = "支付宝根证书文件", dataType = "file", paramType = "form")
+    })
+    @PostMapping("/uploadCerts")
+    public R<Boolean> uploadCerts(
+            @RequestParam Integer id,
+            @RequestParam(value = "appPublicCert", required = false) MultipartFile appPublicCertFile,
+            @RequestParam(value = "alipayPublicCert", required = false) MultipartFile alipayPublicCertFile,
+            @RequestParam(value = "alipayRootCert", required = false) MultipartFile alipayRootCertFile) {
+        log.info("StorePaymentConfigController.uploadCerts id={}", id);
+        if (id == null) {
+            return R.fail("配置ID不能为空");
+        }
+        StorePaymentConfig existing = storePaymentConfigService.getById(id);
+        if (existing == null) {
+            return R.fail("配置不存在");
+        }
+        try {
+            fillCertContent(existing, appPublicCertFile, alipayPublicCertFile, alipayRootCertFile);
+            fillUpdatedUser(existing);
+            boolean ok = storePaymentConfigService.updateById(existing);
+            return ok ? R.success("证书已更新") : R.fail("更新失败");
+        } catch (Exception e) {
+            log.error("上传证书失败", e);
+            return R.fail("上传失败:" + e.getMessage());
+        }
+    }
+
+    @ApiOperationSupport(order = 3)
+    @ApiOperation(value = "将三个证书文件写入指定路径", notes = "从表读出证书内容,写入 targetPath 目录;文件名优先使用表中的 *_cert_name,未设置则用默认 app_public.crt、alipay_public.crt、alipay_root.crt。")
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "id", value = "配置ID", dataType = "int", paramType = "query", required = true),
+            @ApiImplicitParam(name = "targetPath", value = "目标目录(绝对路径或相对路径),如 D:/certs/export 或 /opt/certs", dataType = "string", paramType = "query", required = true)
+    })
+    @GetMapping("/downloadCerts")
+    public R<List<String>> downloadCerts(@RequestParam Integer id, @RequestParam String targetPath) {
+        log.info("StorePaymentConfigController.downloadCerts id={}, targetPath={}", id, targetPath);
+        if (id == null) {
+            return R.fail("配置ID不能为空");
+        }
+        if (targetPath == null || targetPath.trim().isEmpty()) {
+            return R.fail("目标路径不能为空");
+        }
+        if (targetPath.contains("..")) {
+            return R.fail("目标路径不允许包含 ..");
+        }
+        StorePaymentConfig config = storePaymentConfigService.getById(id);
+        if (config == null) {
+            return R.fail("配置不存在");
+        }
+        try {
+            List<String> writtenPaths = writeCertsToPath(config, targetPath.trim());
+            if (writtenPaths.isEmpty()) {
+                return R.fail("该配置下暂无证书内容可写入");
+            }
+            return R.data(writtenPaths);
+        } catch (Exception e) {
+            log.error("写入证书文件失败", e);
+            return R.fail("写入失败:" + e.getMessage());
+        }
+    }
+
+    /** 将表中三个证书内容(byte[] 原样)写入指定目录,不经过字符编码,返回已写入文件的绝对路径列表 */
+    private List<String> writeCertsToPath(StorePaymentConfig config, String targetPath) throws Exception {
+        Path dir = Paths.get(targetPath);
+        Files.createDirectories(dir);
+        List<String> written = new ArrayList<>();
+        if (config.getAppPublicCert() != null && config.getAppPublicCert().length > 0) {
+            String name = safeCertFileName(config.getAppPublicCertName(), "app_public.crt");
+            Path file = dir.resolve(name);
+            Files.write(file, config.getAppPublicCert());
+            written.add(file.toAbsolutePath().toString());
+        }
+        if (config.getAlipayPublicCert() != null && config.getAlipayPublicCert().length > 0) {
+            String name = safeCertFileName(config.getAlipayPublicCertName(), "alipay_public.crt");
+            Path file = dir.resolve(name);
+            Files.write(file, config.getAlipayPublicCert());
+            written.add(file.toAbsolutePath().toString());
+        }
+        if (config.getAlipayRootCert() != null && config.getAlipayRootCert().length > 0) {
+            String name = safeCertFileName(config.getAlipayRootCertName(), "alipay_root.crt");
+            Path file = dir.resolve(name);
+            Files.write(file, config.getAlipayRootCert());
+            written.add(file.toAbsolutePath().toString());
+        }
+        return written;
+    }
+
+    /** 使用表存的文件名(仅取最后一段且不含路径),非法则用默认名 */
+    private String safeCertFileName(String storedName, String defaultName) {
+        if (storedName == null || storedName.trim().isEmpty()) {
+            return defaultName;
+        }
+        String name = storedName.trim();
+        int last = Math.max(name.lastIndexOf('/'), name.lastIndexOf('\\'));
+        if (last >= 0 && last < name.length() - 1) {
+            name = name.substring(last + 1);
+        }
+        if (name.isEmpty() || name.contains("..")) {
+            return defaultName;
+        }
+        return name;
+    }
+
+    /** 将三个证书文件内容原样(byte[])读入实体存入表,不经过字符编码,保证存取一致 */
+    private void fillCertContent(StorePaymentConfig config,
+                                 MultipartFile appPublicCertFile,
+                                 MultipartFile alipayPublicCertFile,
+                                 MultipartFile alipayRootCertFile) throws Exception {
+        if (appPublicCertFile != null && !appPublicCertFile.isEmpty()) {
+            config.setAppPublicCert(appPublicCertFile.getBytes());
+            String fn = appPublicCertFile.getOriginalFilename();
+            if (fn != null && !fn.trim().isEmpty()) {
+                config.setAppPublicCertName(fn.trim());
+            }
+        }
+        if (alipayPublicCertFile != null && !alipayPublicCertFile.isEmpty()) {
+            config.setAlipayPublicCert(alipayPublicCertFile.getBytes());
+            String fn = alipayPublicCertFile.getOriginalFilename();
+            if (fn != null && !fn.trim().isEmpty()) {
+                config.setAlipayPublicCertName(fn.trim());
+            }
+        }
+        if (alipayRootCertFile != null && !alipayRootCertFile.isEmpty()) {
+            config.setAlipayRootCert(alipayRootCertFile.getBytes());
+            String fn = alipayRootCertFile.getOriginalFilename();
+            if (fn != null && !fn.trim().isEmpty()) {
+                config.setAlipayRootCertName(fn.trim());
+            }
+        }
+    }
+
+    @ApiOperationSupport(order = 4)
+    @ApiOperation("删除支付配置")
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "id", value = "配置ID", dataType = "Integer", paramType = "query", required = true)
+    })
+    @PostMapping("/delete")
+    public R<Boolean> delete(@RequestParam Integer id) {
+        log.info("StorePaymentConfigController.delete?id={}", id);
+        if (id == null) {
+            return R.fail("配置ID不能为空");
+        }
+        try {
+            boolean ok = storePaymentConfigService.removeById(id);
+            return ok ? R.success("删除成功") : R.fail("删除失败");
+        } catch (Exception e) {
+            log.error("删除支付配置失败", e);
+            return R.fail("删除失败:" + e.getMessage());
+        }
+    }
+
+    @ApiOperationSupport(order = 5)
+    @ApiOperation("根据ID查询支付配置详情")
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "id", value = "配置ID", dataType = "Integer", paramType = "query", required = true)
+    })
+    @GetMapping("/detail")
+    public R<StorePaymentConfig> detail(@RequestParam Integer id) {
+        log.info("StorePaymentConfigController.detail?id={}", id);
+        if (id == null) {
+            return R.fail("配置ID不能为空");
+        }
+        try {
+            StorePaymentConfig config = storePaymentConfigService.getById(id);
+            if (config == null) {
+                return R.fail("配置不存在");
+            }
+            return R.data(config);
+        } catch (Exception e) {
+            log.error("查询支付配置详情失败", e);
+            return R.fail("查询失败:" + e.getMessage());
+        }
+    }
+
+
+    @ApiOperationSupport(order = 7)
+    @ApiOperation("根据店铺ID查询支付配置")
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "storeId", value = "店铺ID", dataType = "Integer", paramType = "query", required = true)
+    })
+    @GetMapping("/getByStoreId")
+    public R<StorePaymentConfig> getByStoreId(@RequestParam Integer storeId) {
+        log.info("StorePaymentConfigController.getByStoreId?storeId={}", storeId);
+        if (storeId == null) {
+            return R.fail("店铺ID不能为空");
+        }
+        try {
+            StorePaymentConfig config = storePaymentConfigService.getByStoreId(storeId);
+            if (config == null) {
+                return R.fail("该店铺暂无支付配置");
+            }
+            return R.data(config);
+        } catch (Exception e) {
+            log.error("根据店铺ID查询支付配置失败", e);
+            return R.fail("查询失败:" + e.getMessage());
+        }
+    }
+
+    @ApiOperationSupport(order = 8)
+    @ApiOperation("根据店铺用户ID查询支付配置(设置收款账号)")
+    @ApiImplicitParam(name = "storeUserId", value = "店铺用户ID(store_user.id)", dataType = "int", paramType = "query", required = true)
+    @GetMapping("/getByStoreUserId")
+    public R<StorePaymentConfig> getByStoreUserId(@RequestParam Integer storeUserId) {
+        log.info("StorePaymentConfigController.getByStoreUserId storeUserId={}", storeUserId);
+        if (storeUserId == null) {
+            return R.fail("店铺用户ID不能为空");
+        }
+        try {
+            StorePaymentConfig config = storePaymentConfigService.getByStoreUserId(storeUserId);
+            return R.data(config);
+        } catch (Exception e) {
+            log.error("根据店铺用户ID查询支付配置失败", e);
+            return R.fail("查询失败:" + e.getMessage());
+        }
+    }
+
+    @ApiOperationSupport(order = 9)
+    @ApiOperation(value = "设置收款账号-保存微信", notes = "按 store_user_id 新增或更新微信支付参数及证书文件(内容存入表)")
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "storeUserId", value = "店铺用户ID(store_user.id)", dataType = "int", paramType = "form", required = true),
+            @ApiImplicitParam(name = "wechatMiniAppId", value = "微信支付mini appId", dataType = "string", paramType = "form"),
+            @ApiImplicitParam(name = "wechatAppId", value = "微信appId", dataType = "string", paramType = "form"),
+            @ApiImplicitParam(name = "wechatMchId", value = "微信mchId", dataType = "string", paramType = "form"),
+            @ApiImplicitParam(name = "merchantSerialNumber", value = "API证书序列号", dataType = "string", paramType = "form"),
+            @ApiImplicitParam(name = "apiV3Key", value = "APIv3 Key", dataType = "string", paramType = "form"),
+            @ApiImplicitParam(name = "wechatPayPublicKeyId", value = "支付公钥ID", dataType = "string", paramType = "form"),
+            @ApiImplicitParam(name = "wechatPrivateKeyFile", value = "私钥证书", dataType = "file", paramType = "form"),
+            @ApiImplicitParam(name = "wechatPayPublicKeyFile", value = "公钥证书", dataType = "file", paramType = "form")
+    })
+    @PostMapping("/saveWechatByStoreUserId")
+    public R<Integer> saveWechatByStoreUserId(
+            @RequestParam Integer storeUserId,
+            @RequestParam(required = false) String wechatMiniAppId,
+            @RequestParam(required = false) String wechatAppId,
+            @RequestParam(required = false) String wechatMchId,
+            @RequestParam(required = false) String merchantSerialNumber,
+            @RequestParam(required = false) String apiV3Key,
+            @RequestParam(required = false) String wechatPayPublicKeyId,
+            @RequestParam(value = "wechatPrivateKeyFile", required = false) MultipartFile wechatPrivateKeyFile,
+            @RequestParam(value = "wechatPayPublicKeyFile", required = false) MultipartFile wechatPayPublicKeyFile) {
+
+        log.info("StorePaymentConfigController.saveWechatByStoreUserId storeUserId={}", storeUserId);
+        if (storeUserId == null) {
+            return R.fail("店铺用户ID不能为空");
+        }
+        try {
+            StoreUser storeUser = storeUserService.getById(storeUserId);
+            if (storeUser == null) {
+                return R.fail("店铺用户不存在");
+            }
+            StorePaymentConfig config = storePaymentConfigService.getByStoreUserId(storeUserId);
+            if (config == null) {
+                config = new StorePaymentConfig();
+                config.setStoreId(storeUser.getStoreId());
+                config.setStoreUserId(storeUserId);
+                config.setAppId(wechatAppId != null ? wechatAppId : "");
+                fillCreatedUser(config);
+            } else {
+                fillUpdatedUser(config);
+            }
+            config.setWechatMiniAppId(wechatMiniAppId);
+            config.setWechatAppId(wechatAppId);
+            config.setWechatMchId(wechatMchId);
+            config.setMerchantSerialNumber(merchantSerialNumber);
+            config.setApiV3Key(apiV3Key);
+            config.setWechatPayPublicKeyId(wechatPayPublicKeyId);
+            if (wechatPrivateKeyFile != null && !wechatPrivateKeyFile.isEmpty()) {
+                config.setWechatPrivateKeyFile(wechatPrivateKeyFile.getBytes());
+                String fn = wechatPrivateKeyFile.getOriginalFilename();
+                if (fn != null && !fn.trim().isEmpty()) {
+                    config.setWechatPrivateKeyName(fn.trim());
+                }
+            }
+            if (wechatPayPublicKeyFile != null && !wechatPayPublicKeyFile.isEmpty()) {
+                config.setWechatPayPublicKeyFile(wechatPayPublicKeyFile.getBytes());
+                String fn = wechatPayPublicKeyFile.getOriginalFilename();
+                if (fn != null && !fn.trim().isEmpty()) {
+                    config.setWechatPayPublicKeyFileName(fn.trim());
+                }
+            }
+            boolean ok = config.getId() == null ? storePaymentConfigService.save(config) : storePaymentConfigService.updateById(config);
+            if (!ok) {
+                return R.fail("保存失败");
+            }
+            return R.data(config.getId());
+        } catch (Exception e) {
+            log.error("保存微信收款账号失败 storeUserId={}", storeUserId, e);
+            return R.fail("保存失败:" + e.getMessage());
+        }
+    }
+
+    @ApiOperationSupport(order = 10)
+    @ApiOperation(value = "设置收款账号-保存支付宝", notes = "按 store_user_id 新增或更新支付宝支付参数及证书文件(内容存入表)")
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "storeUserId", value = "店铺用户ID(store_user.id)", dataType = "int", paramType = "form", required = true),
+            @ApiImplicitParam(name = "appId", value = "应用ID", dataType = "string", paramType = "form"),
+            @ApiImplicitParam(name = "appSecretCert", value = "应用私钥", dataType = "string", paramType = "form"),
+            @ApiImplicitParam(name = "appPublicCert", value = "应用公钥证书", dataType = "file", paramType = "form"),
+            @ApiImplicitParam(name = "alipayPublicCert", value = "支付宝公钥证书", dataType = "file", paramType = "form"),
+            @ApiImplicitParam(name = "alipayRootCert", value = "支付宝根证书", dataType = "file", paramType = "form")
+    })
+    @PostMapping("/saveAlipayByStoreUserId")
+    public R<Integer> saveAlipayByStoreUserId(
+            @RequestParam Integer storeUserId,
+            @RequestParam(required = false) String appId,
+            @RequestParam(required = false) String appSecretCert,
+            @RequestParam(value = "appPublicCert", required = false) MultipartFile appPublicCertFile,
+            @RequestParam(value = "alipayPublicCert", required = false) MultipartFile alipayPublicCertFile,
+            @RequestParam(value = "alipayRootCert", required = false) MultipartFile alipayRootCertFile) {
+        log.info("StorePaymentConfigController.saveAlipayByStoreUserId storeUserId={}", storeUserId);
+        if (storeUserId == null) {
+            return R.fail("店铺用户ID不能为空");
+        }
+        try {
+            StoreUser storeUser = storeUserService.getById(storeUserId);
+            if (storeUser == null) {
+                return R.fail("店铺用户不存在");
+            }
+            StorePaymentConfig config = storePaymentConfigService.getByStoreUserId(storeUserId);
+            if (config == null) {
+                config = new StorePaymentConfig();
+                config.setStoreId(storeUser.getStoreId());
+                config.setStoreUserId(storeUserId);
+                config.setAppId(appId != null ? appId : "");
+                fillCreatedUser(config);
+            } else {
+                fillUpdatedUser(config);
+            }
+            config.setAppId(appId);
+            config.setAppSecretCert(appSecretCert);
+            fillCertContent(config, appPublicCertFile, alipayPublicCertFile, alipayRootCertFile);
+            boolean ok = config.getId() == null ? storePaymentConfigService.save(config) : storePaymentConfigService.updateById(config);
+            if (!ok) {
+                return R.fail("保存失败");
+            }
+            return R.data(config.getId());
+        } catch (Exception e) {
+            log.error("保存支付宝收款账号失败 storeUserId={}", storeUserId, e);
+            return R.fail("保存失败:" + e.getMessage());
+        }
+    }
+
+    @ApiOperationSupport(order = 12)
+    @ApiOperation("分页查询支付配置列表")
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "page", value = "页码", dataType = "Integer", paramType = "query", defaultValue = "1"),
+            @ApiImplicitParam(name = "size", value = "每页条数", dataType = "Integer", paramType = "query", defaultValue = "10"),
+            @ApiImplicitParam(name = "storeId", value = "店铺ID", dataType = "Integer", paramType = "query"),
+            @ApiImplicitParam(name = "appId", value = "应用ID(模糊)", dataType = "String", paramType = "query")
+    })
+    @GetMapping("/page")
+    public R<IPage<StorePaymentConfig>> page(
+            @RequestParam(defaultValue = "1") Integer page,
+            @RequestParam(defaultValue = "10") Integer size,
+            @RequestParam(required = false) Integer storeId,
+            @RequestParam(required = false) String appId) {
+        log.info("StorePaymentConfigController.page?page={}, size={}, storeId={}, appId={}", page, size, storeId, appId);
+        try {
+            Page<StorePaymentConfig> pageParam = new Page<>(page, size);
+            IPage<StorePaymentConfig> result = storePaymentConfigService.pageList(pageParam, storeId, appId);
+            return R.data(result);
+        } catch (Exception e) {
+            log.error("分页查询支付配置失败", e);
+            return R.fail("查询失败:" + e.getMessage());
+        }
+    }
+
+    @ApiOperationSupport(order = 13)
+    @ApiOperation(value = "新增/更新微信支付配置信息", notes = "按 storeId 查找配置,存在则更新微信字段;不存在则新建一条仅含 storeId 与微信信息的配置。可传表单+两个证书文件(内容存入表)。")
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "storeId", value = "店铺ID", dataType = "int", paramType = "form", required = true),
+            @ApiImplicitParam(name = "wechatAppId", value = "微信appId", dataType = "string", paramType = "form"),
+            @ApiImplicitParam(name = "wechatMchId", value = "微信mchId", dataType = "string", paramType = "form"),
+            @ApiImplicitParam(name = "merchantSerialNumber", value = "微信商户API证书序列号", dataType = "string", paramType = "form"),
+            @ApiImplicitParam(name = "apiV3Key", value = "微信商户的 APIv3 Key", dataType = "string", paramType = "form"),
+            @ApiImplicitParam(name = "wechatPayPublicKeyId", value = "微信支付公钥ID", dataType = "string", paramType = "form"),
+            @ApiImplicitParam(name = "wechatPrivateKeyPath", value = "微信私钥路径", dataType = "string", paramType = "form"),
+            @ApiImplicitParam(name = "wechatPrivateKeyName", value = "微信私钥文件名", dataType = "string", paramType = "form"),
+            @ApiImplicitParam(name = "wechatPayPublicKeyFilePath", value = "微信公钥路径", dataType = "string", paramType = "form"),
+            @ApiImplicitParam(name = "wechatPayPublicKeyFileName", value = "微信公钥文件名", dataType = "string", paramType = "form"),
+            @ApiImplicitParam(name = "wechatPrivateKeyFile", value = "微信私钥文件(内容存入表)", dataType = "file", paramType = "form"),
+            @ApiImplicitParam(name = "wechatPayPublicKeyFile", value = "微信公钥文件(内容存入表)", dataType = "file", paramType = "form")
+    })
+    @PostMapping("/saveWechat")
+    public R<Boolean> saveWechat(
+            @RequestParam Integer storeId,
+            @RequestParam(required = false) String wechatAppId,
+            @RequestParam(required = false) String wechatMchId,
+            @RequestParam(required = false) String merchantSerialNumber,
+            @RequestParam(required = false) String apiV3Key,
+            @RequestParam(required = false) String wechatPayPublicKeyId,
+            @RequestParam(required = false) String wechatPrivateKeyPath,
+            @RequestParam(required = false) String wechatPrivateKeyName,
+            @RequestParam(required = false) String wechatPayPublicKeyFilePath,
+            @RequestParam(required = false) String wechatPayPublicKeyFileName,
+            @RequestParam(value = "wechatPrivateKeyFile", required = false) MultipartFile wechatPrivateKeyFile,
+            @RequestParam(value = "wechatPayPublicKeyFile", required = false) MultipartFile wechatPayPublicKeyFile) {
+        log.info("StorePaymentConfigController.saveWechat storeId={}", storeId);
+        if (storeId == null) {
+            return R.fail("店铺ID不能为空");
+        }
+        try {
+            StorePaymentConfig config = storePaymentConfigService.getByStoreId(storeId);
+            if (config == null) {
+                config = new StorePaymentConfig();
+                config.setStoreId(storeId);
+                config.setAppId(wechatAppId != null ? wechatAppId : "");
+                fillCreatedUser(config);
+            } else {
+                fillUpdatedUser(config);
+            }
+            config.setWechatAppId(wechatAppId);
+            config.setWechatMchId(wechatMchId);
+            config.setMerchantSerialNumber(merchantSerialNumber);
+            config.setApiV3Key(apiV3Key);
+            config.setWechatPayPublicKeyId(wechatPayPublicKeyId);
+            config.setWechatPrivateKeyPath(wechatPrivateKeyPath);
+            config.setWechatPrivateKeyName(wechatPrivateKeyName);
+            config.setWechatPayPublicKeyFilePath(wechatPayPublicKeyFilePath);
+            config.setWechatPayPublicKeyFileName(wechatPayPublicKeyFileName);
+            if (wechatPrivateKeyFile != null && !wechatPrivateKeyFile.isEmpty()) {
+                config.setWechatPrivateKeyFile(wechatPrivateKeyFile.getBytes());
+                if (wechatPrivateKeyName == null || wechatPrivateKeyName.trim().isEmpty()) {
+                    String fn = wechatPrivateKeyFile.getOriginalFilename();
+                    if (fn != null && !fn.trim().isEmpty()) {
+                        config.setWechatPrivateKeyName(fn.trim());
+                    }
+                }
+            }
+            if (wechatPayPublicKeyFile != null && !wechatPayPublicKeyFile.isEmpty()) {
+                config.setWechatPayPublicKeyFile(wechatPayPublicKeyFile.getBytes());
+                if (wechatPayPublicKeyFileName == null || wechatPayPublicKeyFileName.trim().isEmpty()) {
+                    String fn = wechatPayPublicKeyFile.getOriginalFilename();
+                    if (fn != null && !fn.trim().isEmpty()) {
+                        config.setWechatPayPublicKeyFileName(fn.trim());
+                    }
+                }
+            }
+            boolean ok = config.getId() == null ? storePaymentConfigService.save(config) : storePaymentConfigService.updateById(config);
+            return ok ? R.success("微信配置保存成功") : R.fail("保存失败");
+        } catch (Exception e) {
+            log.error("保存微信支付配置失败", e);
+            return R.fail("保存失败:" + e.getMessage());
+        }
+    }
+
+    private void fillCreatedUser(StorePaymentConfig config) {
+        try {
+            JSONObject userInfo = JwtUtil.getCurrentUserInfo();
+            if (userInfo != null) {
+                config.setCreatedUserId(userInfo.getInteger("userId"));
+            }
+        } catch (Exception e) {
+            log.warn("获取当前用户失败: {}", e.getMessage());
+        }
+    }
+
+    private void fillUpdatedUser(StorePaymentConfig config) {
+        try {
+            JSONObject userInfo = JwtUtil.getCurrentUserInfo();
+            if (userInfo != null) {
+                config.setUpdatedUserId(userInfo.getInteger("userId"));
+            }
+        } catch (Exception e) {
+            log.warn("获取当前用户失败: {}", e.getMessage());
+        }
+    }
+}

+ 40 - 0
alien-store/src/main/java/shop/alien/store/service/StorePaymentConfigService.java

@@ -0,0 +1,40 @@
+package shop.alien.store.service;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.extension.service.IService;
+import shop.alien.entity.store.StorePaymentConfig;
+
+/**
+ * 支付配置表 服务类
+ *
+ * @author system
+ */
+public interface StorePaymentConfigService extends IService<StorePaymentConfig> {
+
+    /**
+     * 根据店铺ID查询支付配置
+     *
+     * @param storeId 店铺ID
+     * @return 支付配置,不存在返回 null
+     */
+    StorePaymentConfig getByStoreId(Integer storeId);
+
+    /**
+     * 根据店铺用户ID查询支付配置(设置收款账号场景)
+     *
+     * @param storeUserId 店铺用户ID(store_user.id)
+     * @return 支付配置,不存在返回 null
+     */
+    StorePaymentConfig getByStoreUserId(Integer storeUserId);
+
+    /**
+     * 分页查询支付配置列表
+     *
+     * @param page   分页参数
+     * @param storeId 店铺ID(可选)
+     * @param appId  应用ID(可选,模糊)
+     * @return 分页结果
+     */
+    IPage<StorePaymentConfig> pageList(Page<StorePaymentConfig> page, Integer storeId, String appId);
+}

+ 11 - 0
alien-store/src/main/java/shop/alien/store/service/impl/StoreInfoServiceImpl.java

@@ -180,6 +180,8 @@ public class StoreInfoServiceImpl extends ServiceImpl<StoreInfoMapper, StoreInfo
 
     private final LifeUserViolationMapper lifeUserViolationMapper;
 
+    private final StorePaymentConfigService storePaymentConfigService;
+
 
     @Value("${spring.web.resources.excel-path}")
     private String excelPath;
@@ -1254,6 +1256,7 @@ public class StoreInfoServiceImpl extends ServiceImpl<StoreInfoMapper, StoreInfo
         if (StringUtils.isNotEmpty(storeInfoDto.getStorePositionLongitude()) && StringUtils.isNotEmpty(storeInfoDto.getStorePositionLatitude())) {
             nearMeService.inGeolocation(new Point(Double.parseDouble(storeInfoDto.getStorePositionLongitude()), Double.parseDouble(storeInfoDto.getStorePositionLatitude())), storeInfo.getId().toString(), Boolean.TRUE);
         }
+
         //修改门店店铺用户绑定关系
         String userAccount = storeInfoDto.getUserAccount();
         StoreUser storeUser = storeUserMapper.selectById(userAccount);
@@ -1264,6 +1267,14 @@ public class StoreInfoServiceImpl extends ServiceImpl<StoreInfoMapper, StoreInfo
         if (StringUtils.isNotEmpty(storeInfoDto.getIdCard())) {
             storeUser.setIdCard(storeInfoDto.getIdCard());
         }
+
+        // 通过store_user_id查询支付配置并更新绑定store_id
+        StorePaymentConfig paymentConfig = storePaymentConfigService.getByStoreUserId(storeUser.getId());
+        if (paymentConfig != null) {
+            paymentConfig.setStoreId(storeInfo.getId());
+            storePaymentConfigService.updateById(paymentConfig);
+        }
+
         storeUserMapper.updateById(storeUser);
         //存入店铺营业执照图片
         List<String> businessLicenseAddress = storeInfoDto.getBusinessLicenseAddress();

+ 59 - 0
alien-store/src/main/java/shop/alien/store/service/impl/StorePaymentConfigServiceImpl.java

@@ -0,0 +1,59 @@
+package shop.alien.store.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.StringUtils;
+import shop.alien.entity.store.StorePaymentConfig;
+import shop.alien.mapper.StorePaymentConfigMapper;
+import shop.alien.store.service.StorePaymentConfigService;
+
+/**
+ * 支付配置表 服务实现类
+ *
+ * @author system
+ */
+@Slf4j
+@Service
+@Transactional(rollbackFor = Exception.class)
+@RequiredArgsConstructor
+public class StorePaymentConfigServiceImpl extends ServiceImpl<StorePaymentConfigMapper, StorePaymentConfig> implements StorePaymentConfigService {
+
+    @Override
+    public StorePaymentConfig getByStoreId(Integer storeId) {
+        if (storeId == null) {
+            return null;
+        }
+        LambdaQueryWrapper<StorePaymentConfig> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(StorePaymentConfig::getStoreId, storeId);
+        return this.getOne(wrapper);
+    }
+
+    @Override
+    public StorePaymentConfig getByStoreUserId(Integer storeUserId) {
+        if (storeUserId == null) {
+            return null;
+        }
+        LambdaQueryWrapper<StorePaymentConfig> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(StorePaymentConfig::getStoreUserId, storeUserId);
+        return this.getOne(wrapper);
+    }
+
+    @Override
+    public IPage<StorePaymentConfig> pageList(Page<StorePaymentConfig> page, Integer storeId, String appId) {
+        LambdaQueryWrapper<StorePaymentConfig> wrapper = new LambdaQueryWrapper<>();
+        if (storeId != null) {
+            wrapper.eq(StorePaymentConfig::getStoreId, storeId);
+        }
+        if (StringUtils.hasText(appId)) {
+            wrapper.like(StorePaymentConfig::getAppId, appId);
+        }
+        wrapper.orderByDesc(StorePaymentConfig::getUpdatedTime);
+        return this.page(page, wrapper);
+    }
+}

+ 41 - 0
alien-util/src/main/java/shop/alien/util/typehandler/BlobByteArrayTypeHandler.java

@@ -0,0 +1,41 @@
+package shop.alien.util.typehandler;
+
+import org.apache.ibatis.type.BaseTypeHandler;
+import org.apache.ibatis.type.JdbcType;
+import org.apache.ibatis.type.MappedJdbcTypes;
+import org.apache.ibatis.type.MappedTypes;
+
+import java.sql.CallableStatement;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+
+/**
+ * MyBatis BLOB 与 byte[] 直通类型处理器,不做任何编码转换。
+ * 用于证书等二进制内容:存入与取出字节完全一致,避免 UTF-8/Latin-1 等字符集导致数据损坏。
+ * 使用方式:在实体类字段上添加 @TableField(typeHandler = BlobByteArrayTypeHandler.class)
+ */
+@MappedJdbcTypes(JdbcType.BLOB)
+@MappedTypes(byte[].class)
+public class BlobByteArrayTypeHandler extends BaseTypeHandler<byte[]> {
+
+    @Override
+    public void setNonNullParameter(PreparedStatement ps, int i, byte[] parameter, JdbcType jdbcType) throws SQLException {
+        ps.setBytes(i, parameter);
+    }
+
+    @Override
+    public byte[] getNullableResult(ResultSet rs, String columnName) throws SQLException {
+        return rs.getBytes(columnName);
+    }
+
+    @Override
+    public byte[] getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
+        return rs.getBytes(columnIndex);
+    }
+
+    @Override
+    public byte[] getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
+        return cs.getBytes(columnIndex);
+    }
+}

+ 45 - 0
alien-util/src/main/java/shop/alien/util/typehandler/StringBlobTypeHandler.java

@@ -0,0 +1,45 @@
+package shop.alien.util.typehandler;
+
+import org.apache.ibatis.type.BaseTypeHandler;
+import org.apache.ibatis.type.JdbcType;
+import org.apache.ibatis.type.MappedJdbcTypes;
+import org.apache.ibatis.type.MappedTypes;
+
+import java.nio.charset.StandardCharsets;
+import java.sql.CallableStatement;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+
+/**
+ * MyBatis BLOB 与 String 互转类型处理器(UTF-8)。
+ * 用于证书等大文本字段在数据库中存为 BLOB、在 Java 实体中为 String 的场景。
+ * 使用方式:在实体类字段上添加 @TableField(typeHandler = StringBlobTypeHandler.class)
+ */
+@MappedJdbcTypes(JdbcType.BLOB)
+@MappedTypes(String.class)
+public class StringBlobTypeHandler extends BaseTypeHandler<String> {
+
+    @Override
+    public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException {
+        ps.setBytes(i, parameter.getBytes(StandardCharsets.UTF_8));
+    }
+
+    @Override
+    public String getNullableResult(ResultSet rs, String columnName) throws SQLException {
+        byte[] bytes = rs.getBytes(columnName);
+        return bytes == null ? null : new String(bytes, StandardCharsets.UTF_8);
+    }
+
+    @Override
+    public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
+        byte[] bytes = rs.getBytes(columnIndex);
+        return bytes == null ? null : new String(bytes, StandardCharsets.UTF_8);
+    }
+
+    @Override
+    public String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
+        byte[] bytes = cs.getBytes(columnIndex);
+        return bytes == null ? null : new String(bytes, StandardCharsets.UTF_8);
+    }
+}