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:
@@ -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";
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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),设备可在新场所用新激活码重新绑定
|
||||
|
||||
@@ -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位随机数
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,11 +9,6 @@ import java.util.Map;
|
||||
*/
|
||||
public interface WechatPayService {
|
||||
|
||||
/**
|
||||
* 检查微信支付配置是否完整(可用于判断是否走 Mock 逻辑)
|
||||
*/
|
||||
boolean isConfigured();
|
||||
|
||||
/**
|
||||
* 创建 Native 支付订单(扫码支付)
|
||||
*
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user