This commit is contained in:
van
2026-05-09 22:57:53 +08:00
parent f4697481fa
commit 30ff4077fe
14 changed files with 744 additions and 55 deletions

View File

@@ -0,0 +1,81 @@
package com.ruoyi.jarvis.domain;
import com.ruoyi.common.annotation.Excel;
import com.ruoyi.common.core.domain.BaseEntity;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
/**
* TG 黄牛电话库 jarvis_tg_scalper_phone
*/
public class TgScalperPhone extends BaseEntity
{
private static final long serialVersionUID = 1L;
private Long id;
/** 11位手机号 */
@Excel(name = "手机号")
private String phone;
/** 命中时直接回复企微的备注 */
@Excel(name = "备注")
private String remark;
/** 0禁用 1启用 */
@Excel(name = "状态", readConverterExp = "0=禁用,1=启用")
private Integer status;
public Long getId()
{
return id;
}
public void setId(Long id)
{
this.id = id;
}
public String getPhone()
{
return phone;
}
public void setPhone(String phone)
{
this.phone = phone;
}
public String getRemark()
{
return remark;
}
public void setRemark(String remark)
{
this.remark = remark;
}
public Integer getStatus()
{
return status;
}
public void setStatus(Integer status)
{
this.status = status;
}
@Override
public String toString()
{
return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE)
.append("id", getId())
.append("phone", getPhone())
.append("remark", getRemark())
.append("status", getStatus())
.append("createTime", getCreateTime())
.append("updateTime", getUpdateTime())
.toString();
}
}

View File

@@ -0,0 +1,28 @@
package com.ruoyi.jarvis.mapper;
import java.util.List;
import com.ruoyi.jarvis.domain.TgScalperPhone;
/**
* TG 黄牛电话库 Mapper
*/
public interface TgScalperPhoneMapper
{
TgScalperPhone selectTgScalperPhoneById(Long id);
List<TgScalperPhone> selectTgScalperPhoneList(TgScalperPhone q);
/** 按手机号查(不限状态,供唯一性校验) */
TgScalperPhone selectTgScalperPhoneByPhone(String phone);
/** 启用状态下按手机号查(开/慢走前置) */
TgScalperPhone selectEnabledByPhone(String phone);
int insertTgScalperPhone(TgScalperPhone row);
int updateTgScalperPhone(TgScalperPhone row);
int deleteTgScalperPhoneById(Long id);
int deleteTgScalperPhoneByIds(Long[] ids);
}

View File

@@ -0,0 +1,18 @@
package com.ruoyi.jarvis.service;
import java.util.List;
/**
* 「开/慢开」TG 查询完成后,经 wxSend 主动推送文本(已按企微单条上限切分)。
* 实现类在 ruoyi-admin委托 WxSendWeComPushClient。
*/
public interface IPhoneForwardActivePush {
/**
* 异步排队推送到企微成员
*
* @param toUser 成员 UserIDFromUserName
* @param chunks 每段不超过 UTF-8 2048 字节的正文
*/
void schedulePushChunks(String toUser, List<String> chunks);
}

View File

@@ -0,0 +1,25 @@
package com.ruoyi.jarvis.service;
import java.util.List;
import com.ruoyi.jarvis.domain.TgScalperPhone;
/**
* TG 黄牛电话库
*/
public interface ITgScalperPhoneService
{
TgScalperPhone selectTgScalperPhoneById(Long id);
List<TgScalperPhone> selectTgScalperPhoneList(TgScalperPhone q);
/** 启用状态命中(供 phone-forward 前置) */
TgScalperPhone selectEnabledByPhone(String phone);
int insertTgScalperPhone(TgScalperPhone row);
int updateTgScalperPhone(TgScalperPhone row);
int deleteTgScalperPhoneByIds(Long[] ids);
int deleteTgScalperPhoneById(Long id);
}

View File

@@ -3,10 +3,19 @@ package com.ruoyi.jarvis.service.impl;
import com.alibaba.fastjson2.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Resource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import com.ruoyi.jarvis.domain.TgScalperPhone;
import com.ruoyi.jarvis.domain.dto.WeComInboundRequest;
import com.ruoyi.jarvis.service.IPhoneForwardActivePush;
import com.ruoyi.jarvis.service.ITgScalperPhoneService;
import com.ruoyi.jarvis.util.WeComUtf8ChunkUtil;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
@@ -15,6 +24,11 @@ import java.net.URL;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
@@ -30,6 +44,12 @@ import java.util.regex.Pattern;
* 在同一会话内多次收取(仅一次发送 query按第 2 条是否已为结果决定在 2/3 条间取值,避免重复计费。
* 「慢开」返回仍会去掉尾部固定推广行。
* </p>
* <p>
* <b>计费幂等</b>:与 {@link java.util.concurrent.locks.ReentrantLock} 只能串行化并发、不能防止企微对同一 {@code MsgId} 的重试。
* 开启 {@code jarvis.phone-forward.dedup-enabled} 后,对同一 msgIdwxSend 已传入)在 TTL 内复用首次成功结果,
* 并在获取锁后再次查缓存,避免「上一请求已付费返回、排队中的重试」再次调用 Telegram。
* 开启 {@code jarvis.phone-forward.async-result-push-enabled} 时被动立即回复「收到电话…」TG 结果经 wxSend 主动推送。
* </p>
*/
@Service
public class OpenPhoneForwardService {
@@ -96,17 +116,63 @@ public class OpenPhoneForwardService {
@Value("${jarvis.phone-forward.circuit-open-ms:120000}")
private long circuitOpenMs;
/** 为 true 时先查黄牛电话库,命中且备注非空则不再请求 Telegram */
@Value("${jarvis.phone-forward.scalper-library-enabled:true}")
private boolean scalperLibraryEnabled;
/**
* 同一企微 MsgId 在 TTL 内只走一次付费转发(成功结果缓存;错误文案不缓存以便用户重试)
*/
@Value("${jarvis.phone-forward.dedup-enabled:true}")
private boolean dedupEnabled;
@Value("${jarvis.phone-forward.dedup-ttl-seconds:900}")
private int dedupTtlSeconds;
/**
* 为 true 时立即被动回复「收到电话」TG 在后台查询并通过 wxSend 主动推送全文
*/
@Value("${jarvis.phone-forward.async-result-push-enabled:true}")
private boolean asyncResultPushEnabled;
@Resource
private ITgScalperPhoneService tgScalperPhoneService;
@Autowired(required = false)
private IPhoneForwardActivePush phoneForwardActivePush;
/** 仅允许单飞:所有 phone-forward 请求串行 */
private final ReentrantLock tgBridgeCallLock = new ReentrantLock(true);
/** 异步 TG 查询进行中(与幂等 dedup 使用同一关联键) */
private final ConcurrentHashMap<String, Long> forwardInflightUntilMs = new ConcurrentHashMap<>();
private static final class DedupEntry {
final String reply;
final long expireAtMs;
DedupEntry(String reply, long expireAtMs) {
this.reply = reply;
this.expireAtMs = expireAtMs;
}
}
/** key → 已成功返回给用户的正文(含黄牛库与 TG */
private final ConcurrentHashMap<String, DedupEntry> forwardReplyDedup = new ConcurrentHashMap<>();
private final AtomicInteger circuitFailureCount = new AtomicInteger(0);
/** 熔断恢复时间epoch ms0 表示未熔断 */
private final AtomicLong circuitOpenUntilMs = new AtomicLong(0);
/**
* @param req 须含 contentmsgId 由 wxSend 透传时可用于幂等防重复计费
* @return 非 null 表示本条消息已由本服务处理含错误提示null 表示不匹配规则
*/
public String tryReply(String rawContent) {
public String tryReply(WeComInboundRequest req) {
if (req == null) {
return null;
}
String rawContent = req.getContent();
if (!enabled || !StringUtils.hasText(baseUrl) || rawContent == null) {
return null;
}
@@ -123,7 +189,115 @@ public class OpenPhoneForwardService {
if (phone == null) {
return null;
}
return doForward(phone, bot);
final String cKey = resolveCorrelationKey(req, text, phone, bot);
if (dedupEnabled) {
String cached = dedupGet(cKey);
if (cached != null) {
log.info("phone-forward 幂等命中 key={} phone={} bot={}", cKey, phone, bot);
if (asyncResultPushEnabled) {
return "该消息已查询过,结果已通过应用消息推送,请在会话中查看。";
}
return cached;
}
}
if (asyncResultPushEnabled && isForwardInflight(cKey)) {
return "查询进行中,请稍候查看应用消息。";
}
if (scalperLibraryEnabled) {
TgScalperPhone hit = tgScalperPhoneService.selectEnabledByPhone(phone);
if (hit != null && StringUtils.hasText(hit.getRemark())) {
String remark = hit.getRemark().trim();
log.info("phone-forward 黄牛电话库命中 phone={},跳过 Telegram", phone);
dedupPut(cKey, remark);
return remark;
}
}
if (asyncResultPushEnabled && phoneForwardActivePush != null && StringUtils.hasText(req.getFromUserName())) {
markForwardInflight(cKey);
final String toUser = req.getFromUserName().trim();
CompletableFuture.runAsync(() -> runForwardAndPush(toUser, phone, bot, cKey));
return String.format("收到电话:%s。\n后续结果将通过应用消息推送。", phone);
}
return doForward(phone, bot, dedupEnabled ? cKey : null);
}
private static String resolveCorrelationKey(WeComInboundRequest req, String text, String phone, String bot) {
String msgId = req.getMsgId() != null ? req.getMsgId().trim() : "";
if (StringUtils.hasText(msgId)) {
return "pf:msg:" + msgId;
}
String from = req.getFromUserName() != null ? req.getFromUserName().trim() : "";
return "pf:h:" + from + ":" + Integer.toHexString(Objects.hash(text, phone, bot));
}
private boolean isForwardInflight(String cKey) {
Long until = forwardInflightUntilMs.get(cKey);
if (until == null) {
return false;
}
if (System.currentTimeMillis() > until) {
forwardInflightUntilMs.remove(cKey, until);
return false;
}
return true;
}
private void markForwardInflight(String cKey) {
long until = System.currentTimeMillis() + Math.max(120_000L, (long) dedupTtlSeconds * 1000L);
forwardInflightUntilMs.put(cKey, until);
}
private void clearForwardInflight(String cKey) {
forwardInflightUntilMs.remove(cKey);
}
private void runForwardAndPush(String toUser, String phone, String bot, String cKey) {
try {
String reply = doForward(phone, bot, dedupEnabled ? cKey : null);
List<String> chunks = WeComUtf8ChunkUtil.splitUtf8Chunks(reply, WeComUtf8ChunkUtil.WE_COM_TEXT_MAX_UTF8_BYTES);
chunks.removeIf(s -> !StringUtils.hasText(s));
if (chunks.isEmpty()) {
chunks = Collections.singletonList("(无返回内容)");
}
phoneForwardActivePush.schedulePushChunks(toUser, chunks);
} catch (Exception e) {
log.warn("phone-forward 异步推送异常 phone={} err={}", phone, e.toString());
} finally {
clearForwardInflight(cKey);
}
}
private String dedupGet(String key) {
if (!dedupEnabled || key == null) {
return null;
}
DedupEntry e = forwardReplyDedup.get(key);
if (e == null) {
return null;
}
if (System.currentTimeMillis() > e.expireAtMs) {
forwardReplyDedup.remove(key, e);
return null;
}
return e.reply;
}
private void dedupPut(String key, String reply) {
if (key == null || !dedupEnabled || !StringUtils.hasText(reply) || !shouldCacheReplyForDedup(reply)) {
return;
}
long ttlMs = Math.max(60_000L, (long) dedupTtlSeconds * 1000L);
forwardReplyDedup.put(key, new DedupEntry(reply, System.currentTimeMillis() + ttlMs));
}
/** 失败提示不缓存,避免用户无法通过重试恢复 */
private static boolean shouldCacheReplyForDedup(String reply) {
return !reply.startsWith("「转发服务」");
}
private boolean isCircuitOpen() {
@@ -165,7 +339,10 @@ public class OpenPhoneForwardService {
return null;
}
private String doForward(String phone, String bot) {
/**
* @param dedupKey 非空且开启幂等时:持锁后再次查缓存,避免首请求已付费、重试排队后二次请求 Telegram
*/
private String doForward(String phone, String bot, String dedupKey) {
if (isCircuitOpen()) {
log.warn("phone-forward 熔断拒绝 phone={} bot={}", phone, bot);
return "「转发服务」暂时不可用(连续失败保护中),请一分钟后再试。";
@@ -185,7 +362,16 @@ public class OpenPhoneForwardService {
return "「转发服务」正忙(上一条查询尚未结束),请稍后再试。";
}
}
return doForwardUnsynchronized(phone, bot);
if (dedupKey != null) {
String again = dedupGet(dedupKey);
if (again != null) {
log.info("phone-forward 持锁后幂等命中,跳过本次 HTTP/Telegram phone={} bot={}", phone, bot);
return again;
}
}
String reply = doForwardUnsynchronized(phone, bot);
dedupPut(dedupKey, reply);
return reply;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.warn("phone-forward 获取锁被打断 phone={} bot={}", phone, bot);

View File

@@ -0,0 +1,100 @@
package com.ruoyi.jarvis.service.impl;
import java.util.List;
import java.util.regex.Pattern;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.jarvis.domain.TgScalperPhone;
import com.ruoyi.jarvis.mapper.TgScalperPhoneMapper;
import com.ruoyi.jarvis.service.ITgScalperPhoneService;
@Service
public class TgScalperPhoneServiceImpl implements ITgScalperPhoneService
{
private static final Pattern MOBILE_11 = Pattern.compile("^1\\d{10}$");
@Autowired
private TgScalperPhoneMapper tgScalperPhoneMapper;
@Override
public TgScalperPhone selectTgScalperPhoneById(Long id)
{
return tgScalperPhoneMapper.selectTgScalperPhoneById(id);
}
@Override
public List<TgScalperPhone> selectTgScalperPhoneList(TgScalperPhone q)
{
return tgScalperPhoneMapper.selectTgScalperPhoneList(q);
}
@Override
public TgScalperPhone selectEnabledByPhone(String phone)
{
if (!StringUtils.hasText(phone))
{
return null;
}
return tgScalperPhoneMapper.selectEnabledByPhone(phone.trim());
}
@Override
public int insertTgScalperPhone(TgScalperPhone row)
{
validatePhone(row.getPhone());
if (!StringUtils.hasText(row.getRemark()))
{
throw new ServiceException("备注不能为空");
}
TgScalperPhone exist = tgScalperPhoneMapper.selectTgScalperPhoneByPhone(row.getPhone().trim());
if (exist != null)
{
throw new ServiceException("手机号已存在");
}
if (row.getStatus() == null)
{
row.setStatus(1);
}
row.setPhone(row.getPhone().trim());
return tgScalperPhoneMapper.insertTgScalperPhone(row);
}
@Override
public int updateTgScalperPhone(TgScalperPhone row)
{
validatePhone(row.getPhone());
if (!StringUtils.hasText(row.getRemark()))
{
throw new ServiceException("备注不能为空");
}
TgScalperPhone other = tgScalperPhoneMapper.selectTgScalperPhoneByPhone(row.getPhone().trim());
if (other != null && !other.getId().equals(row.getId()))
{
throw new ServiceException("手机号已存在");
}
row.setPhone(row.getPhone().trim());
return tgScalperPhoneMapper.updateTgScalperPhone(row);
}
@Override
public int deleteTgScalperPhoneByIds(Long[] ids)
{
return tgScalperPhoneMapper.deleteTgScalperPhoneByIds(ids);
}
@Override
public int deleteTgScalperPhoneById(Long id)
{
return tgScalperPhoneMapper.deleteTgScalperPhoneById(id);
}
private static void validatePhone(String phone)
{
if (!StringUtils.hasText(phone) || !MOBILE_11.matcher(phone.trim()).matches())
{
throw new ServiceException("请输入正确的11位手机号");
}
}
}

View File

@@ -9,15 +9,14 @@ import com.ruoyi.jarvis.service.ILogisticsService;
import com.ruoyi.jarvis.service.IWeComChatSessionService;
import com.ruoyi.jarvis.service.IWeComInboundService;
import com.ruoyi.jarvis.service.SuperAdminService;
import com.ruoyi.jarvis.util.WeComUtf8ChunkUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import javax.annotation.Resource;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -25,7 +24,7 @@ import java.util.regex.Pattern;
/**
* LinPingFan全部指令其他人员须在超级管理员中识别为本人wxid=企微 UserID**或** 企微 UserID 出现在 touser 逗号分隔列表中),且仅「京*」指令 + 京东分享物流链接流程;
* 例外:以「单」或「开始」开头且含「分销标记」的录单正文优先于物流(不进入 3.cn 多轮、不占用物流监听)。
* 以「开」或「慢开」开头且正文含 11 位手机号1 开头):POST 配置项 jarvis.phone-forward 指向的局域网服务,回显 reply_textbody 含对应 bot
* 以「开」或「慢开」开头且正文含 11 位手机号1 开头):TG 查询默认先被动回复「收到电话」,结果经 wxSend 主动推送;本地库命中仍为被动全文
* 多轮会话使用 Redis{@link WeComChatSession},键 interaction_state:wecom:{FromUserName}与旧版「开通礼金」interaction_state 思路一致。
* 回复正文按 UTF-8 每段至多 2048 字节拆分:首段被动回复,其余主动推送(同一次用户消息、不重复触发查询)。
*/
@@ -38,9 +37,6 @@ public class WeComInboundServiceImpl implements IWeComInboundService {
private static final Pattern JD_3CN = Pattern.compile("https://3\\.cn/[A-Za-z0-9\\-]+");
/** 企微被动回复与应用文本消息 content 官方上限UTF-8 字节(见被动回复 / 发送应用消息文档) */
private static final int WE_COM_TEXT_MAX_UTF8_BYTES = 2048;
/** 无超级管理员配置 */
private static String replyPermissionDenied() {
return "「权限说明」\n\n"
@@ -109,7 +105,7 @@ public class WeComInboundServiceImpl implements IWeComInboundService {
return WeComInboundResult.passiveOnly(replyPermissionDenied());
}
String openPhoneReply = openPhoneForwardService.tryReply(content);
String openPhoneReply = openPhoneForwardService.tryReply(req);
if (openPhoneReply != null) {
return toChunkedInboundResult(openPhoneReply);
}
@@ -180,7 +176,7 @@ public class WeComInboundServiceImpl implements IWeComInboundService {
if (parts.size() == 1) {
return toChunkedInboundResult(parts.get(0));
}
List<String> headChunks = splitUtf8Chunks(parts.get(0), WE_COM_TEXT_MAX_UTF8_BYTES);
List<String> headChunks = WeComUtf8ChunkUtil.splitUtf8Chunks(parts.get(0), WeComUtf8ChunkUtil.WE_COM_TEXT_MAX_UTF8_BYTES);
String passive = headChunks.isEmpty() ? "" : headChunks.get(0);
List<String> active = new ArrayList<>();
for (int h = 1; h < headChunks.size(); h++) {
@@ -191,7 +187,7 @@ public class WeComInboundServiceImpl implements IWeComInboundService {
if (p == null) {
continue;
}
for (String chunk : splitUtf8Chunks(p, WE_COM_TEXT_MAX_UTF8_BYTES)) {
for (String chunk : WeComUtf8ChunkUtil.splitUtf8Chunks(p, WeComUtf8ChunkUtil.WE_COM_TEXT_MAX_UTF8_BYTES)) {
active.add(chunk);
}
}
@@ -202,7 +198,7 @@ public class WeComInboundServiceImpl implements IWeComInboundService {
* 首段 ≤2048 UTF-8 字节走被动回复,其余走 wxSend 主动推送(同一次用户消息内顺序下发,不重复计费)。
*/
private static WeComInboundResult toChunkedInboundResult(String fullText) {
List<String> chunks = splitUtf8Chunks(fullText, WE_COM_TEXT_MAX_UTF8_BYTES);
List<String> chunks = WeComUtf8ChunkUtil.splitUtf8Chunks(fullText, WeComUtf8ChunkUtil.WE_COM_TEXT_MAX_UTF8_BYTES);
if (chunks.isEmpty()) {
return WeComInboundResult.passiveOnly("");
}
@@ -212,47 +208,6 @@ public class WeComInboundServiceImpl implements IWeComInboundService {
return new WeComInboundResult(chunks.get(0), new ArrayList<>(chunks.subList(1, chunks.size())));
}
/**
* 按 UTF-8 字节长度切分,每段不超过 maxUtf8Bytes不在 BMP 的码点按整字符保留)。
*/
private static List<String> splitUtf8Chunks(String text, int maxUtf8Bytes) {
if (text == null) {
return Collections.singletonList("");
}
if (text.isEmpty()) {
return Collections.singletonList("");
}
if (maxUtf8Bytes < 1) {
throw new IllegalArgumentException("maxUtf8Bytes must be >= 1");
}
List<String> out = new ArrayList<>();
int i = 0;
final int n = text.length();
while (i < n) {
int chunkStart = i;
int usedBytes = 0;
while (i < n) {
int cp = text.codePointAt(i);
int charCount = Character.charCount(cp);
int b = new String(Character.toChars(cp)).getBytes(StandardCharsets.UTF_8).length;
if (usedBytes + b > maxUtf8Bytes) {
break;
}
usedBytes += b;
i += charCount;
}
if (i == chunkStart) {
int cp = text.codePointAt(i);
int charCount = Character.charCount(cp);
out.add(text.substring(chunkStart, chunkStart + charCount));
i = chunkStart + charCount;
} else {
out.add(text.substring(chunkStart, i));
}
}
return out;
}
/**
* 录单正文(指令层走「单…」写库)优先于物流:与 {@link InstructionServiceImpl} 新模板一致。
*/

View File

@@ -0,0 +1,59 @@
package com.ruoyi.jarvis.util;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* 企微文本按 UTF-8 字节切分(与应用消息 / 被动回复上限一致)。
*/
public final class WeComUtf8ChunkUtil {
/** 企微文本 content 官方上限约 2048 UTF-8 字节 */
public static final int WE_COM_TEXT_MAX_UTF8_BYTES = 2048;
private WeComUtf8ChunkUtil() {
}
/**
* 按 UTF-8 字节长度切分,每段不超过 maxUtf8Bytes非 BMP 码点按整字符保留)。
*/
public static List<String> splitUtf8Chunks(String text, int maxUtf8Bytes) {
if (text == null) {
return Collections.singletonList("");
}
if (text.isEmpty()) {
return Collections.singletonList("");
}
if (maxUtf8Bytes < 1) {
throw new IllegalArgumentException("maxUtf8Bytes must be >= 1");
}
List<String> out = new ArrayList<>();
int i = 0;
final int n = text.length();
while (i < n) {
int chunkStart = i;
int usedBytes = 0;
while (i < n) {
int cp = text.codePointAt(i);
int charCount = Character.charCount(cp);
int b = new String(Character.toChars(cp)).getBytes(StandardCharsets.UTF_8).length;
if (usedBytes + b > maxUtf8Bytes) {
break;
}
usedBytes += b;
i += charCount;
}
if (i == chunkStart) {
int cp = text.codePointAt(i);
int charCount = Character.charCount(cp);
out.add(text.substring(chunkStart, chunkStart + charCount));
i = chunkStart + charCount;
} else {
out.add(text.substring(chunkStart, i));
}
}
return out;
}
}

View File

@@ -0,0 +1,85 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.jarvis.mapper.TgScalperPhoneMapper">
<resultMap type="TgScalperPhone" id="TgScalperPhoneResult">
<result property="id" column="id" />
<result property="phone" column="phone" />
<result property="remark" column="remark" />
<result property="status" column="status" />
<result property="createTime" column="create_time" />
<result property="updateTime" column="update_time" />
</resultMap>
<sql id="selectTgScalperPhoneVo">
select id, phone, remark, status, create_time, update_time
from jarvis_tg_scalper_phone
</sql>
<select id="selectTgScalperPhoneList" parameterType="TgScalperPhone" resultMap="TgScalperPhoneResult">
<include refid="selectTgScalperPhoneVo"/>
<where>
<if test="phone != null and phone != ''"> and phone like concat('%', #{phone}, '%')</if>
<if test="remark != null and remark != ''"> and remark like concat('%', #{remark}, '%')</if>
<if test="status != null"> and status = #{status}</if>
</where>
order by update_time desc, id desc
</select>
<select id="selectTgScalperPhoneById" parameterType="Long" resultMap="TgScalperPhoneResult">
<include refid="selectTgScalperPhoneVo"/>
where id = #{id}
</select>
<select id="selectTgScalperPhoneByPhone" parameterType="String" resultMap="TgScalperPhoneResult">
<include refid="selectTgScalperPhoneVo"/>
where phone = #{phone}
limit 1
</select>
<select id="selectEnabledByPhone" parameterType="String" resultMap="TgScalperPhoneResult">
<include refid="selectTgScalperPhoneVo"/>
where phone = #{phone} and status = 1
limit 1
</select>
<insert id="insertTgScalperPhone" parameterType="TgScalperPhone" useGeneratedKeys="true" keyProperty="id">
insert into jarvis_tg_scalper_phone
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="phone != null and phone != ''">phone,</if>
<if test="remark != null">remark,</if>
<if test="status != null">status,</if>
create_time,
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="phone != null and phone != ''">#{phone},</if>
<if test="remark != null">#{remark},</if>
<if test="status != null">#{status},</if>
sysdate(),
</trim>
</insert>
<update id="updateTgScalperPhone" parameterType="TgScalperPhone">
update jarvis_tg_scalper_phone
<trim prefix="SET" suffixOverrides=",">
<if test="phone != null and phone != ''">phone = #{phone},</if>
<if test="remark != null">remark = #{remark},</if>
<if test="status != null">status = #{status},</if>
update_time = sysdate(),
</trim>
where id = #{id}
</update>
<delete id="deleteTgScalperPhoneById" parameterType="Long">
delete from jarvis_tg_scalper_phone where id = #{id}
</delete>
<delete id="deleteTgScalperPhoneByIds" parameterType="Long">
delete from jarvis_tg_scalper_phone where id in
<foreach item="id" collection="array" open="(" separator="," close=")">
#{id}
</foreach>
</delete>
</mapper>