feat: 移除mock支付、卡不可复用、新增设备端取消订单接口

1. 移除所有mock支付逻辑,订单强制走微信支付
2. 卡状态流转改为单向:UNUSED->RESERVED->USED(终态不可复用)
3. 解绑接口改为仅清除device_id,卡状态保持USED
4. 新增设备端取消订单接口 POST /api/jst/daycard/cancel/{orderNo}
5. 修复微信回调验签header大小写问题
6. 临时注释分账标记(商户号未开通分账权限)
7. 删除多余的App UI辅助接口(ApiJstUiController)
8. 清理application.yml中无用UI配置
This commit is contained in:
zhaogd
2026-08-06 13:43:34 +08:00
parent ec1f300c53
commit 8385196b38
13 changed files with 100 additions and 462 deletions

View File

@@ -159,8 +159,6 @@ public final class DayCardConstants {
public static final String REFUND_STATUS_CLOSED = "CLOSED";
// ==================== 支付渠道 ====================
/** Mock支付 */
public static final String PAY_CHANNEL_MOCK = "MOCK";
/** 微信支付 */
public static final String PAY_CHANNEL_WECHAT = "WECHAT";
}

View File

@@ -125,7 +125,7 @@ public class DayCardManageController {
/**
* 解绑设备天卡权益(Web端手动操作)
* <p>将设备绑定的 USED 卡转为 REUSABLE,释放设备。橘视厅权益保留,后续新设备下单可复用此卡。</p>
* <p>清除卡上的 device_id 绑定,卡状态保持 USED 不变,不可被其他设备复用。</p>
*/
@AuditAction(value = "DAYCARD_DEVICE_UNBIND", description = "解绑设备天卡权益")
@PostMapping("/device/unbind")

View File

@@ -56,7 +56,7 @@ public class DayCardOrderEntity implements Serializable {
private Date createdAt;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date updatedAt;
/** 支付渠道 MOCK/WECHAT */
/** 支付渠道 WECHAT */
private String payChannel;
/** 微信预支付ID */
private String prepayId;

View File

@@ -66,6 +66,15 @@ public interface DayCardOrderService extends IService<DayCardOrderEntity> {
*/
DayCardOrderEntity getByOrderNo(String orderNo);
/**
* 设备端取消未支付订单
* <p>校验订单归属设备后,关单微信支付、释放卡、更新订单CANCELLED、清理Redis锁</p>
*
* @param orderNo 订单号
* @param deviceId 设备ID(从 sign 解析,校验订单归属)
*/
void cancelOrder(String orderNo, Long deviceId);
/**
* 查询设备的天卡权益(激活后 App 获取播放凭证)
* <p>从 day_card_pool 表查询 status=USED 且 expire_at > NOW() 的卡,返回 thirdCardNo 和 expireAt</p>
@@ -86,7 +95,7 @@ public interface DayCardOrderService extends IService<DayCardOrderEntity> {
/**
* 解绑设备天卡权益(Web端手动操作)
* <p>将 USED 卡转为 REUSABLE,释放设备绑定,保留橘视厅权益供后续设备复用</p>
* <p>清除卡上的 device_id 绑定,卡状态保持 USED 不变,不可被其他设备复用</p>
*
* @param deviceId 设备ID
*/

View File

@@ -165,7 +165,7 @@ public class DayCardOrderServiceImpl extends ServiceImpl<DayCardOrderDao, DayCar
order.setChannelOrgId(channelOrgId);
order.setAmount(amount);
order.setStatus(DayCardConstants.ORDER_STATUS_CREATED);
order.setPayChannel("MOCK");
order.setPayChannel(DayCardConstants.PAY_CHANNEL_WECHAT);
order.setDeleteFlag(DayCardConstants.DELETE_FLAG_UNDEL);
Date now = new Date();
order.setCreatedAt(now);
@@ -179,35 +179,12 @@ public class DayCardOrderServiceImpl extends ServiceImpl<DayCardOrderDao, DayCar
baseMapper.insert(order);
// 7. 微信 Native 支付下单
if (wechatPayService.isConfigured()) {
try {
String codeUrl = wechatPayService.createNativePay(orderNo, "天卡订单", amount, channelOrgId, agentOrgId, storeOrgId);
order.setCodeUrl(codeUrl);
order.setPayChannel("WECHAT");
UpdateWrapper<DayCardOrderEntity> payUpdate = new UpdateWrapper<>();
payUpdate.eq("id", order.getId())
.set("code_url", codeUrl)
.set("pay_channel", "WECHAT");
.set("code_url", codeUrl);
baseMapper.update(null, payUpdate);
} catch (Exception e) {
logger.warn("微信下单失败,回退到MOCK模式: orderNo={}", orderNo, e);
order.setPayChannel("MOCK");
order.setCodeUrl("mock://pay?orderNo=" + orderNo);
UpdateWrapper<DayCardOrderEntity> payUpdate = new UpdateWrapper<>();
payUpdate.eq("id", order.getId())
.set("code_url", order.getCodeUrl())
.set("pay_channel", "MOCK");
baseMapper.update(null, payUpdate);
}
} else {
order.setPayChannel("MOCK");
order.setCodeUrl("mock://pay?orderNo=" + orderNo);
UpdateWrapper<DayCardOrderEntity> payUpdate = new UpdateWrapper<>();
payUpdate.eq("id", order.getId())
.set("code_url", order.getCodeUrl())
.set("pay_channel", "MOCK");
baseMapper.update(null, payUpdate);
}
// 8. SKIP LOCKED 抢卡(通用池,不按 partner 过滤)
DayCardPoolEntity pool = dayCardPoolDao.selectOneForUpdateSkipLocked();
@@ -219,7 +196,7 @@ public class DayCardOrderServiceImpl extends ServiceImpl<DayCardOrderDao, DayCar
// 9. 乐观锁更新卡状态 UNUSED -> RESERVED
UpdateWrapper<DayCardPoolEntity> poolUpdate = new UpdateWrapper<>();
poolUpdate.eq("id", pool.getId())
.in("status", DayCardConstants.POOL_STATUS_UNUSED, DayCardConstants.POOL_STATUS_REUSABLE)
.eq("status", DayCardConstants.POOL_STATUS_UNUSED)
.eq("delete_flag", DayCardConstants.DELETE_FLAG_UNDEL)
.set("status", DayCardConstants.POOL_STATUS_RESERVED)
.set("order_no", orderNo)
@@ -298,7 +275,7 @@ public class DayCardOrderServiceImpl extends ServiceImpl<DayCardOrderDao, DayCar
order.setChannelOrgId(channelOrgId);
order.setAmount(amount);
order.setStatus(DayCardConstants.ORDER_STATUS_CREATED);
order.setPayChannel("MOCK");
order.setPayChannel(DayCardConstants.PAY_CHANNEL_WECHAT);
order.setDeleteFlag(DayCardConstants.DELETE_FLAG_UNDEL);
Date now = new Date();
order.setCreatedAt(now);
@@ -314,36 +291,12 @@ public class DayCardOrderServiceImpl extends ServiceImpl<DayCardOrderDao, DayCar
baseMapper.insert(order);
// 6.5 微信 Native 支付下单
if (wechatPayService.isConfigured()) {
try {
String codeUrl = wechatPayService.createNativePay(orderNo, "天卡订单", amount, channelOrgId, agentOrgId, storeOrgId);
order.setCodeUrl(codeUrl);
order.setPayChannel("WECHAT");
UpdateWrapper<DayCardOrderEntity> payUpdate = new UpdateWrapper<>();
payUpdate.eq("id", order.getId())
.set("code_url", codeUrl)
.set("pay_channel", "WECHAT");
.set("code_url", codeUrl);
baseMapper.update(null, payUpdate);
} catch (Exception e) {
logger.warn("微信下单失败,回退到MOCK模式: orderNo={}", orderNo, e);
order.setPayChannel("MOCK");
order.setCodeUrl("mock://pay?orderNo=" + orderNo);
UpdateWrapper<DayCardOrderEntity> payUpdate = new UpdateWrapper<>();
payUpdate.eq("id", order.getId())
.set("code_url", order.getCodeUrl())
.set("pay_channel", "MOCK");
baseMapper.update(null, payUpdate);
}
} else {
// 开发模式:使用 Mock 支付
order.setPayChannel("MOCK");
order.setCodeUrl("mock://pay?orderNo=" + orderNo);
UpdateWrapper<DayCardOrderEntity> payUpdate = new UpdateWrapper<>();
payUpdate.eq("id", order.getId())
.set("code_url", order.getCodeUrl())
.set("pay_channel", "MOCK");
baseMapper.update(null, payUpdate);
}
// 7. SKIP LOCKED 抢卡
DayCardPoolEntity pool = dayCardPoolDao.selectOneForUpdateSkipLocked();
@@ -355,7 +308,7 @@ public class DayCardOrderServiceImpl extends ServiceImpl<DayCardOrderDao, DayCar
// 8. 乐观锁更新卡状态 UNUSED -> RESERVED
UpdateWrapper<DayCardPoolEntity> poolUpdate = new UpdateWrapper<>();
poolUpdate.eq("id", pool.getId())
.in("status", DayCardConstants.POOL_STATUS_UNUSED, DayCardConstants.POOL_STATUS_REUSABLE)
.eq("status", DayCardConstants.POOL_STATUS_UNUSED)
.eq("delete_flag", DayCardConstants.DELETE_FLAG_UNDEL)
.set("status", DayCardConstants.POOL_STATUS_RESERVED)
.set("order_no", orderNo)
@@ -444,12 +397,12 @@ public class DayCardOrderServiceImpl extends ServiceImpl<DayCardOrderDao, DayCar
redisUtils.delete(DayCardConstants.REDIS_DEVICE_ORDER_PREFIX + order.getDeviceSn());
}
// 6. 触发分账(异步)
try {
profitSharingExecuteService.executeSharing(orderNo);
} catch (Exception e) {
logger.error("触发分账失败: orderNo={}", orderNo, e);
}
// 6. 触发分账(商户号开通分账权限后取消注释启用)
// try {
// profitSharingExecuteService.executeSharing(orderNo);
// } catch (Exception e) {
// logger.error("触发分账失败: orderNo={}", orderNo, e);
// }
}
/**
@@ -646,7 +599,7 @@ public class DayCardOrderServiceImpl extends ServiceImpl<DayCardOrderDao, DayCar
// 更新订单为CANCELLED
UpdateWrapper<DayCardOrderEntity> orderUpdate = new UpdateWrapper<>();
orderUpdate.eq("id", order.getId())
.eq("status", DayCardConstants.ORDER_STATUS_CREATED)
.in("status", DayCardConstants.ORDER_STATUS_CREATED, DayCardConstants.ORDER_STATUS_PAYING)
.set("status", DayCardConstants.ORDER_STATUS_CANCELLED)
.set("cancel_time", now)
.set("updated_at", now);
@@ -672,6 +625,29 @@ public class DayCardOrderServiceImpl extends ServiceImpl<DayCardOrderDao, DayCar
return baseMapper.selectOne(wrapper);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void cancelOrder(String orderNo, Long deviceId) {
if (StringUtils.isBlank(orderNo) || deviceId == null) {
throw new ASException("订单号和设备ID不能为空");
}
DayCardOrderEntity order = getByOrderNo(orderNo);
if (order == null) {
throw new ASException("订单不存在: " + orderNo);
}
if (!deviceId.equals(order.getDeviceId())) {
throw new ASException("无权操作此订单");
}
if (DayCardConstants.ORDER_STATUS_PAID.equals(order.getStatus())) {
throw new ASException("订单已支付,无法取消");
}
if (DayCardConstants.ORDER_STATUS_CANCELLED.equals(order.getStatus())) {
return;
}
cancelSingleOrder(order);
logger.info("设备端取消订单: orderNo={}, deviceId={}", orderNo, deviceId);
}
@Override
public DayCardPoolEntity getDeviceBenefits(Long deviceId) {
QueryWrapper<DayCardPoolEntity> wrapper = new QueryWrapper<>();
@@ -706,7 +682,6 @@ public class DayCardOrderServiceImpl extends ServiceImpl<DayCardOrderDao, DayCar
UpdateWrapper<DayCardPoolEntity> update = new UpdateWrapper<>();
update.eq("id", pool.getId())
.eq("status", DayCardConstants.POOL_STATUS_USED)
.set("status", DayCardConstants.POOL_STATUS_REUSABLE)
.set("device_id", null)
.set("updated_at", new Date());
int updated = dayCardPoolDao.update(null, update);
@@ -723,17 +698,16 @@ public class DayCardOrderServiceImpl extends ServiceImpl<DayCardOrderDao, DayCar
throw new ASException("设备ID不能为空");
}
Date now = new Date();
// 1. 释放设备当前天卡权益(若有):USED -> REUSABLE,清卡上的 device_id
// 1. 解绑设备当前天卡权益(若有):清除 device_id,卡状态保持 USED 不变
DayCardPoolEntity pool = getDeviceBenefits(deviceId);
if (pool != null) {
UpdateWrapper<DayCardPoolEntity> poolUpdate = new UpdateWrapper<>();
poolUpdate.eq("id", pool.getId())
.eq("status", DayCardConstants.POOL_STATUS_USED)
.set("status", DayCardConstants.POOL_STATUS_REUSABLE)
.set("device_id", null)
.set("updated_at", now);
dayCardPoolDao.update(null, poolUpdate);
logger.info("设备组织解绑-释放天卡权益: deviceId={}, poolId={}, cardCode={}",
logger.info("设备组织解绑-解除天卡权益绑定: deviceId={}, poolId={}, cardCode={}",
deviceId, pool.getId(), pool.getCardCode());
}
// 2. 清除设备的门店组织关联(storeOrgId),设备可在新场所用新激活码重新绑定

View File

@@ -157,18 +157,13 @@ public class DayCardRefundServiceImpl extends ServiceImpl<DayCardRefundDao, DayC
// 7. 尝试回退分账(设置关联分账单状态为 CLOSED)
rollbackSettleRecords(orderNo, now);
// 8. 调用微信退款(Mock支付则跳过)
String payNo = order.getPayNo();
if (isWechatPayment(payNo)) {
// 8. 调用微信退款
BigDecimal totalAmount = order.getAmount();
WechatPayResult refundResult = wechatPayService.refund(orderNo, refundNo, refundAmount, totalAmount, reason);
if (!refundResult.isSuccess()) {
throw new ASException("微信退款失败: " + refundResult.getErrorMsg());
}
logger.info("微信退款成功: orderNo={}, refundNo={}", orderNo, refundNo);
} else {
logger.info("Mock支付退款,无需调用微信接口: orderNo={}, refundNo={}", orderNo, refundNo);
}
// 9. 更新退款记录 status=SUCCESS
refund.setStatus(DayCardConstants.REFUND_STATUS_SUCCESS);
@@ -274,21 +269,6 @@ public class DayCardRefundServiceImpl extends ServiceImpl<DayCardRefundDao, DayC
}
}
/**
* 判断是否为微信支付
* <p>Mock 支付的 payNo 以 "MOCK_" 开头,其余视为微信支付</p>
*
* @param payNo 支付单号
* @return true=微信支付
*/
private boolean isWechatPayment(String payNo) {
if (StringUtils.isBlank(payNo)) {
// 没有支付单号,默认视为微信支付
return true;
}
return !payNo.startsWith(DayCardConstants.PAY_CHANNEL_MOCK + "_");
}
/**
* 生成退款单号:REFUND + yyyyMMddHHmmss + 6位随机数
*/

View File

@@ -195,6 +195,28 @@ public class ApiJstController {
return buildSuccessResponse(data);
}
/**
* 取消天卡订单(用户放弃支付时调用)
*/
@ApiOperation(value = "取消天卡订单",
notes = "请求头必传 sign。用户取消支付时调用,取消订单并释放卡,允许设备重新下单。")
@ApiImplicitParams({
@ApiImplicitParam(name = "sign", value = "设备签名", required = true, paramType = "header", dataType = "string")
})
@PostMapping("/daycard/cancel/{orderNo}")
public Map<String, Object> cancelDayCard(@PathVariable("orderNo") String orderNo, HttpServletRequest request) {
try {
Long deviceId = deviceIdFrom(request);
dayCardOrderService.cancelOrder(orderNo, deviceId);
Map<String, Object> data = new HashMap<>();
data.put("orderNo", orderNo);
data.put("status", "CANCELLED");
return buildSuccessResponse(data);
} catch (ASException e) {
return rrExceptionResponse(e);
}
}
/**
* 设备心跳:更新设备心跳时间,续期 sign
*/

View File

@@ -1,253 +0,0 @@
package io.as.modules.openapi.controller;
import io.as.modules.device.dto.DeviceOrgContext;
import io.as.modules.device.service.DeviceResolveService;
import io.as.modules.openapi.filter.ApiDeviceSignFilter;
import io.as.modules.sys.entity.SysOrgEntity;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 橘视听 App UI 数据接口
* <p>提供顶栏按钮、筛选分类、筛选内容列表、独立配置等 App 展示所需数据</p>
* <p>所有接口均需请求头 sign(由 ApiDeviceSignFilter 校验)</p>
* <p>返回格式:{code:0, msg:"success", data:{...}}</p>
*/
@RestController
@RequestMapping("/api/jst")
public class ApiJstUiController {
private static final Logger logger = LoggerFactory.getLogger(ApiJstUiController.class);
@Autowired
private DeviceResolveService deviceResolveService;
@Value("${renren.daycard.ui.logo-url:https://daycard.oss.cn/logo.png}")
private String defaultLogoUrl;
@Value("${renren.daycard.ui.login-bg-url:https://daycard.oss.cn/login_bg.png}")
private String defaultLoginBgUrl;
@Value("${renren.daycard.ui.search-normal-url:https://daycard.oss.cn/search_normal.png}")
private String searchNormalUrl;
@Value("${renren.daycard.ui.search-focus-url:https://daycard.oss.cn/search_focus.png}")
private String searchFocusUrl;
@Value("${renren.daycard.ui.help-normal-url:https://daycard.oss.cn/help_normal.png}")
private String helpNormalUrl;
@Value("${renren.daycard.ui.help-focus-url:https://daycard.oss.cn/help_focus.png}")
private String helpFocusUrl;
// ==================== 1. 顶栏按钮 ====================
/**
* 顶栏按钮数据:返回 logo + 搜索 + 帮助
* <p>logo 优先取设备所属渠道的 brand_logo_url,无则用全局默认</p>
*/
@PostMapping("/topButtons")
public Map<String, Object> topButtons(@RequestBody(required = false) Map<String, Object> body,
HttpServletRequest request) {
Long deviceId = deviceIdFrom(request);
String logoUrl = resolveLogoUrl(deviceId);
List<Map<String, Object>> buttons = new ArrayList<>();
// type=1 Logo(仅 back_url)
Map<String, Object> logoBtn = new HashMap<>();
logoBtn.put("type", 1);
logoBtn.put("back_url", logoUrl);
buttons.add(logoBtn);
// type=3 搜索(back_url + focus_picurl)
Map<String, Object> searchBtn = new HashMap<>();
searchBtn.put("type", 3);
searchBtn.put("back_url", searchNormalUrl);
searchBtn.put("focus_picurl", searchFocusUrl);
buttons.add(searchBtn);
// type=6 帮助(back_url + focus_picurl)
Map<String, Object> helpBtn = new HashMap<>();
helpBtn.put("type", 6);
helpBtn.put("back_url", helpNormalUrl);
helpBtn.put("focus_picurl", helpFocusUrl);
buttons.add(helpBtn);
return buildJstResponse(buttons);
}
// ==================== 2. 筛选分类 ====================
/**
* 筛选分类:返回栏目/类型/地区/年代四级分类
* <p>当前为固定分类数据,后续可对接橘视厅媒资 API</p>
*/
@PostMapping("/filter/tags")
public Map<String, Object> filterTags(@RequestBody(required = false) Map<String, Object> body) {
List<Map<String, Object>> series = new ArrayList<>();
// type=1 栏目
series.add(buildSeries(1, "全部栏目", new String[][]{
{"all", "全部"}, {"movie", "电影"}, {"tv", "电视剧"}, {"variety", "综艺"}, {"anime", "动漫"}
}));
// type=2 类型
series.add(buildSeries(2, "全部类型", new String[][]{
{"all", "全部"}, {"action", "动作"}, {"comedy", "喜剧"}, {"romance", "爱情"},
{"scifi", "科幻"}, {"suspense", "悬疑"}, {"animation", "动画"}
}));
// type=3 地区
series.add(buildSeries(3, "全部地区", new String[][]{
{"all", "全部"}, {"cn", "内地"}, {"hk", "中国香港"}, {"tw", "中国台湾"},
{"us", "美国"}, {"kr", "韩国"}, {"jp", "日本"}, {"uk", "英国"}
}));
// type=4 年代
series.add(buildSeries(4, "全部年代", new String[][]{
{"all", "全部"}, {"2026", "2026"}, {"2025", "2025"}, {"2024", "2024"},
{"2023", "2023"}, {"2022", "2022"}, {"2021", "2021"}
}));
Map<String, Object> data = new HashMap<>();
data.put("series", series);
return buildJstResponse(data);
}
// ==================== 3. 筛选内容列表 ====================
/**
* 筛选内容列表:根据筛选条件返回内容列表
* <p>TODO: 后续对接橘视厅媒资 API 或自建媒资表,当前返回 mock 数据骨架</p>
*/
@PostMapping("/filter/content")
public Map<String, Object> filterContent(@RequestBody Map<String, Object> body) {
int page = parseInt(body.get("page"), 1);
int pageSize = parseInt(body.get("pageSize"), 20);
// TODO: 对接真实媒资数据源,当前返回 mock 数据
List<Map<String, Object>> list = new ArrayList<>();
String[][] mockContents = {
{"MOV001", "测试影片一", "https://daycard.oss.cn/poster/1.jpg", "8", "true"},
{"MOV002", "测试影片二", "https://daycard.oss.cn/poster/2.jpg", "1", "false"},
{"MOV003", "测试影片三", "https://daycard.oss.cn/poster/3.jpg", "8", "true"},
{"MOV004", "测试影片四", "https://daycard.oss.cn/poster/4.jpg", "1", "false"},
{"MOV005", "测试影片五", "https://daycard.oss.cn/poster/5.jpg", "8", "true"}
};
for (String[] item : mockContents) {
Map<String, Object> content = new HashMap<>();
content.put("contentCode", item[0]);
content.put("title", item[1]);
content.put("posterUrl", item[2]);
content.put("parentContentType", item[3]);
content.put("isFree", Boolean.parseBoolean(item[4]));
list.add(content);
}
Map<String, Object> data = new HashMap<>();
data.put("total", 5);
data.put("currentPage", page);
data.put("totalPage", 1);
data.put("list", list);
return buildJstResponse(data);
}
// ==================== 4. 独立配置接口 ====================
/**
* 独立配置:返回 logoUrl + loginBg
* <p>logoUrl 优先取设备所属渠道的 brand_logo_url</p>
*/
@GetMapping("/config")
public Map<String, Object> config(@RequestParam(required = false) String deviceId, HttpServletRequest request) {
Long devId = deviceIdFrom(request);
if (devId == null && deviceId != null) {
try {
devId = Long.parseLong(deviceId);
} catch (NumberFormatException ignored) {
}
}
String logoUrl = resolveLogoUrl(devId);
Map<String, Object> data = new HashMap<>();
data.put("logoUrl", logoUrl);
data.put("loginBg", defaultLoginBgUrl);
return buildJstResponse(data);
}
// ==================== 内部方法 ====================
/**
* 从设备所属渠道解析 logoUrl,无则用全局默认
*/
private String resolveLogoUrl(Long deviceId) {
if (deviceId == null) {
return defaultLogoUrl;
}
try {
DeviceOrgContext ctx = deviceResolveService.resolveDevice(deviceId);
SysOrgEntity channelOrg = ctx.getChannelOrg();
if (channelOrg != null && channelOrg.getBrandLogoUrl() != null
&& !channelOrg.getBrandLogoUrl().trim().isEmpty()) {
return channelOrg.getBrandLogoUrl().trim();
}
} catch (Exception e) {
logger.warn("解析设备渠道logo失败,使用默认logo: deviceId={}", deviceId, e);
}
return defaultLogoUrl;
}
/**
* 构建筛选分类维度
*/
private Map<String, Object> buildSeries(int type, String name, String[][] items) {
Map<String, Object> series = new HashMap<>();
series.put("type", type);
series.put("name", name);
List<Map<String, String>> itemList = new ArrayList<>();
for (String[] item : items) {
Map<String, String> m = new HashMap<>();
m.put("id", item[0]);
m.put("name", item[1]);
itemList.add(m);
}
series.put("items", itemList);
return series;
}
/**
* 构建统一响应
*/
private static Map<String, Object> buildJstResponse(Object data) {
Map<String, Object> result = new HashMap<>();
result.put("code", 0);
result.put("msg", "success");
result.put("data", data);
return result;
}
private static Long deviceIdFrom(HttpServletRequest request) {
return (Long) request.getAttribute(ApiDeviceSignFilter.REQUEST_ATTR_DEVICE_ID);
}
private static int parseInt(Object val, int defaultValue) {
if (val == null) {
return defaultValue;
}
try {
return Integer.parseInt(String.valueOf(val));
} catch (NumberFormatException e) {
return defaultValue;
}
}
}

View File

@@ -26,7 +26,6 @@ import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.math.BigDecimal;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
@@ -160,33 +159,6 @@ public class PaymentController {
return R.dataSuccess(data);
}
/**
* Mock 支付(仅开发测试用,直接触发发卡逻辑,绕过微信支付)
* <p>用法:POST /openapi/payment/mock-pay/{orderNo}</p>
*/
@ApiOperation("Mock支付(仅开发测试)")
@AuditAction(value = "PAYMENT_MOCK", description = "Mock支付(测试)")
@PostMapping("/mock-pay/{orderNo}")
public R mockPay(@PathVariable("orderNo") String orderNo) {
DayCardOrderEntity order = dayCardOrderService.getByOrderNo(orderNo);
if (order == null) {
throw new ASException("订单不存在: " + orderNo);
}
// 设置支付渠道为 MOCK
order.setPayChannel("MOCK");
dayCardOrderService.updateById(order);
// 直接触发发卡逻辑
String mockPayNo = "MOCK_" + System.currentTimeMillis();
dayCardOrderService.payCallback(orderNo, mockPayNo, order.getAmount());
Map<String, Object> data = new HashMap<>();
data.put("orderNo", orderNo);
data.put("payNo", mockPayNo);
data.put("message", "Mock支付成功,发卡逻辑已触发");
return R.dataSuccess(data);
}
/**
* 微信退款回调通知
* <p>无需签名鉴权,微信服务器直接调用。</p>
@@ -271,14 +243,15 @@ public class PaymentController {
/**
* 提取请求头(用于微信验签)
* <p>HttpServletRequest.getHeader() 大小写不敏感,直接按标准名称提取,
* 避免 HashMap 大小写敏感导致取不到值</p>
*/
private Map<String, String> extractHeaders(HttpServletRequest request) {
Map<String, String> headers = new HashMap<>();
Enumeration<String> headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()) {
String name = headerNames.nextElement();
headers.put(name, request.getHeader(name));
}
headers.put("Wechatpay-Serial", request.getHeader("Wechatpay-Serial"));
headers.put("Wechatpay-Nonce", request.getHeader("Wechatpay-Nonce"));
headers.put("Wechatpay-Signature", request.getHeader("Wechatpay-Signature"));
headers.put("Wechatpay-Timestamp", request.getHeader("Wechatpay-Timestamp"));
return headers;
}
}

View File

@@ -9,11 +9,6 @@ import java.util.Map;
*/
public interface WechatPayService {
/**
* 检查微信支付配置是否完整(可用于判断是否走 Mock 逻辑)
*/
boolean isConfigured();
/**
* 创建 Native 支付订单(扫码支付)
*

View File

@@ -108,10 +108,10 @@ public class WechatPayServiceImpl implements WechatPayService {
payAmount.setCurrency("CNY");
request.setAmount(payAmount);
// 标记订单支持分账,支付成功后可调用分账 API
SettleInfo settleInfo = new SettleInfo();
settleInfo.setProfitSharing(true);
request.setSettleInfo(settleInfo);
// 分账标记:商户号开通分账权限后取消下方注释即可启用
// SettleInfo settleInfo = new SettleInfo();
// settleInfo.setProfitSharing(true);
// request.setSettleInfo(settleInfo);
PrepayResponse response = service.prepay(request);
String codeUrl = response.getCodeUrl();
@@ -262,10 +262,6 @@ public class WechatPayServiceImpl implements WechatPayService {
if (StringUtils.isBlank(orderNo)) {
return false;
}
if (!isConfigured()) {
logger.info("微信关单-Mock模式(未配置微信支付): orderNo={}", orderNo);
return true;
}
logger.info("微信关单: orderNo={}", orderNo);
try {
WechatPayConfigEntity dbCfg = resolveConfigByOrderNo(orderNo);
@@ -287,11 +283,6 @@ public class WechatPayServiceImpl implements WechatPayService {
@Override
public WechatPayResult refund(String orderNo, String refundNo, BigDecimal refundAmount, BigDecimal totalAmount, String reason) {
if (!isConfigured()) {
int refundFen = yuanToFen(refundAmount);
logger.info("微信退款-Mock模式: orderNo={}, refundNo={}, refundAmount={}分, reason={}", orderNo, refundNo, refundFen, reason);
return WechatPayResult.ok(orderNo, refundNo, refundAmount);
}
logger.info("微信退款: orderNo={}, refundNo={}, refundAmount={}", orderNo, refundNo, refundAmount);
try {
// TODO: 接入 wechatpay-java RefundService
@@ -313,11 +304,6 @@ public class WechatPayServiceImpl implements WechatPayService {
*/
@Override
public WechatPayResult createProfitSharing(String transactionId, String outOrderNo, List<Map<String, Object>> receivers) {
if (!isConfigured()) {
logger.info("微信分账-Mock模式: transactionId={}, outOrderNo={}, receivers={}", transactionId, outOrderNo, receivers != null ? receivers.size() : 0);
return WechatPayResult.ok(outOrderNo, outOrderNo, null);
}
int receiverCount = receivers != null ? receivers.size() : 0;
logger.info("微信分账: transactionId={}, outOrderNo={}, receivers={}", transactionId, outOrderNo, receiverCount);
@@ -370,10 +356,6 @@ public class WechatPayServiceImpl implements WechatPayService {
*/
@Override
public boolean finishProfitSharing(String outOrderNo, String transactionId) {
if (!isConfigured()) {
logger.info("完结分账-Mock模式: outOrderNo={}, transactionId={}", outOrderNo, transactionId);
return true;
}
logger.info("完结分账: outOrderNo={}, transactionId={}", outOrderNo, transactionId);
try {
Config config = getOrCreateConfig();
@@ -402,10 +384,6 @@ public class WechatPayServiceImpl implements WechatPayService {
*/
@Override
public WechatPayResult queryProfitSharing(String outOrderNo) {
if (!isConfigured()) {
logger.info("查询分账-Mock模式: outOrderNo={}", outOrderNo);
return WechatPayResult.ok(outOrderNo, outOrderNo, null);
}
logger.info("查询分账: outOrderNo={}", outOrderNo);
try {
Config config = getOrCreateConfig();
@@ -429,10 +407,6 @@ public class WechatPayServiceImpl implements WechatPayService {
*/
@Override
public WechatPayResult profitSharingReturn(String outOrderNo, String receiverAccount, int amount, String outReturnNo) {
if (!isConfigured()) {
logger.info("分账回退-Mock模式: outOrderNo={}, receiverAccount={}, amount={}分, outReturnNo={}", outOrderNo, receiverAccount, amount, outReturnNo);
return WechatPayResult.ok(outReturnNo, outReturnNo, null);
}
logger.info("分账回退: outOrderNo={}, receiverAccount={}, amount={}分", outOrderNo, receiverAccount, amount);
try {
Config config = getOrCreateConfig();
@@ -460,10 +434,6 @@ public class WechatPayServiceImpl implements WechatPayService {
*/
@Override
public WechatPayResult addReceiver(String receiverType, String receiverAccount, String receiverName, String relationType) {
if (!isConfigured()) {
logger.info("添加分账接收方-Mock模式: type={}, account={}, name={}, relation={}", receiverType, receiverAccount, receiverName, relationType);
return WechatPayResult.ok(null, "MOCK_BIND_" + System.currentTimeMillis(), null);
}
logger.info("添加分账接收方: type={}, account={}, name={}", receiverType, receiverAccount, receiverName);
try {
WechatPayConfigEntity dbCfg = getActiveDbConfig();
@@ -494,10 +464,6 @@ public class WechatPayServiceImpl implements WechatPayService {
*/
@Override
public WechatPayResult deleteReceiver(String receiverType, String receiverAccount) {
if (!isConfigured()) {
logger.info("删除分账接收方-Mock模式: type={}, account={}", receiverType, receiverAccount);
return WechatPayResult.ok(null, receiverAccount, null);
}
logger.info("删除分账接收方: type={}, account={}", receiverType, receiverAccount);
try {
WechatPayConfigEntity dbCfg = getActiveDbConfig();
@@ -520,22 +486,9 @@ public class WechatPayServiceImpl implements WechatPayService {
// ==================== 配置管理 ====================
@Override
public boolean isConfigured() {
try {
validateConfig();
return true;
} catch (Exception e) {
return false;
}
}
/**
* 校验微信支付配置是否完整(DB 配置优先,回退 yml 环境变量)
*/
private void validateConfig() {
validateConfig(null, null, null);
}
private void validateConfig(Long channelOrgId, Long agentOrgId, Long storeOrgId) {
WechatPayConfigEntity dbCfg = getActiveDbConfig(channelOrgId, agentOrgId, storeOrgId);

View File

@@ -70,14 +70,6 @@ renren:
card-valid-hours: ${DAYCARD_VALID_HOURS:24}
# 超时取消任务每次处理数量
cancel-batch-size: ${DAYCARD_CANCEL_BATCH:200}
# App UI 资源配置(顶栏按钮图标、登录背景图)
ui:
logo-url: ${UI_LOGO_URL:https://daycard.oss-cn-hangzhou.aliyuncs.com/logo.png}
login-bg-url: ${UI_LOGIN_BG_URL:https://daycard.oss-cn-hangzhou.aliyuncs.com/login_bg.png}
search-normal-url: ${UI_SEARCH_NORMAL:https://daycard.oss-cn-hangzhou.aliyuncs.com/search_normal.png}
search-focus-url: ${UI_SEARCH_FOCUS:https://daycard.oss-cn-hangzhou.aliyuncs.com/search_focus.png}
help-normal-url: ${UI_HELP_NORMAL:https://daycard.oss-cn-hangzhou.aliyuncs.com/help_normal.png}
help-focus-url: ${UI_HELP_FOCUS:https://daycard.oss-cn-hangzhou.aliyuncs.com/help_focus.png}
# 微信支付配置
wechat:
pay:

View File

@@ -7,13 +7,8 @@
<select id="selectOneForUpdateSkipLocked" resultType="io.as.modules.daycard.entity.DayCardPoolEntity">
SELECT * FROM day_card_pool
WHERE delete_flag = 'UNDEL'
AND (
status = 'UNUSED'
OR (status = 'REUSABLE' AND expire_at > NOW())
)
ORDER BY
CASE WHEN status = 'REUSABLE' THEN 0 ELSE 1 END,
id ASC
AND status = 'UNUSED'
ORDER BY id ASC
LIMIT 1
FOR UPDATE SKIP LOCKED
</select>