首页 > 数据库 > 数据库开发技术 > 代码回现 | 如何实现交易反欺诈?
<Topics enabled="true"> <properties> <property name="port">9999</property> <property name="group.initial.rebalance.delay.ms">0</property> <property name="retention.policy.threads">1</property> </properties> <profiles> <profile name="retain_compact"> <retention policy="compact" limit="2048" /> </profile> </profiles> </Topics>
CREATE Topic TRAINTOPIC execute procedure train_events.insert; CREATE TOPIC RECHARGE execute procedure RechargeCard; CREATE TOPIC using stream CARD_ALERT_EXPORT properties(topic.format=avro); create topic using stream FRAUD properties(topic.format=avro,consumer.keys=TRANS_ID);
csvloader --file $PROJ_HOME/data/redline.csv --reportdir log stations csvloader --file $PROJ_HOME/data/trains.csv --reportdir log trains
java metro.pub.TrainProducer localhost:9999 TRAINTOPIC 8
public static void main(String[] args) {
ScheduledExecutorService EXECUTOR = Executors.newScheduledThreadPool(10);
TrainProducer producer = new TrainProducer(args[0], args[1], Integer.parseInt(args[2]));
System.out.println("Scheduling trains");
EXECUTOR.scheduleAtFixedRate (
() -> {
producer.publish(producer.getNewEvents());
}, 1, 50, MILLISECONDS);
}private Producer<String, TrainEvent> createProducer() {
Properties props = new Properties();
props.put("bootstrap.servers", brokers);
props.put("acks", "all");
props.put("retries", 0);
props.put("batch.size", 16384);
props.put("linger.ms", 1);
props.put("buffer.memory", 33554432);
props.put("key.serializer",
"org.apache.kafka.common.serialization.StringSerializer");
props.put("value.serializer",
"metro.serde.TrainEventSer");
Producer<String, TrainEvent> producer = new KafkaProducer
<String, TrainEvent>(props);
return producer;
} static HashMap<Integer, Station> idToStationMap = new HashMap<>();
static {
idToStationMap.put(1, new Station(1, 1200000, 450000));
idToStationMap.put(2, new Station(2, 1050000, 250000));
idToStationMap.put(3, new Station(3, 850000, 300000));
idToStationMap.put(4, new Station(4, 900000, 350000));
idToStationMap.put(5, new Station(5, 500000, 260000));
idToStationMap.put(6, new Station(6, 950000, 190000));
idToStationMap.put(7, new Station(7, 450000, 130000));
idToStationMap.put(8, new Station(8, 200000, 280000));
idToStationMap.put(9, new Station(9, 200000, 110000));
idToStationMap.put(10, new Station(10, 450000, 300000));
idToStationMap.put(11, new Station(11, 550000, 200000));
idToStationMap.put(12, new Station(12, 550000, 200000));
idToStationMap.put(13, new Station(13, 800000, 150000));
idToStationMap.put(14, new Station(14, 950000, 100000));
idToStationMap.put(15, new Station(15, 1000000, 130000));
idToStationMap.put(16, new Station(16, 1200000, 220000));
idToStationMap.put(17, new Station(17, 1500000, 500000));
}
public static class Station {
public final int stationId;
public final int nextStnDuration;
public final int stnWaitDuration;
public Station(int stationId, int nextStnDuration, int stnWaitDuration) {
this.stationId = stationId;
this.nextStnDuration = nextStnDuration;
this.stnWaitDuration = stnWaitDuration;
}
}public List<TrainEvent> getNewEvents() {
ArrayList<TrainEvent> records = new ArrayList<>();
for(TrainEvent trainEvent : idToTrainMap.values()) {
LastKnownLocation prevLoc = trainEvent.location;
LastKnownLocation curLoc = next(prevLoc, LocalDateTime.now());
if(!prevLoc.equals(curLoc)) {
trainEvent = new TrainEvent(trainEvent.trainId, curLoc);
idToTrainMap.put(trainEvent.trainId, trainEvent);
records.add(trainEvent);
}
}
return records;
}CREATE Topic TRAINTOPIC execute procedure train_events.insert;
java metro.pub.CardsProducer --mode=recharge --servers=localhost:9999 --Topic=RECHARGE
public static void main(String[] args) throws IOException {
CONFIG.parse("CardsProducer", args);
if(CONFIG.mode.equals("new")) {
genCards(CONFIG);
return;
}
ScheduledExecutorService EXECUTOR = Executors.newScheduledThreadPool(10);
CardsProducer producer = new CardsProducer(CONFIG.servers, CONFIG.Topic);
System.out.println("Recharging Cards");
EXECUTOR.scheduleAtFixedRate (
() -> {
producer.publish(producer.getRechargeActivityRecords(1));
}, 1, 5, MILLISECONDS);
} public List<CardEvent> getRechargeActivityRecords(int count) {
final ArrayList<CardEvent> records = new ArrayList<>();
int amt = (ThreadLocalRandom.current().nextInt(18)+2)*1000;
int stationId = ThreadLocalRandom.current().nextInt(1, 18);
ThreadLocalRandom.current().ints(count, 0, CONFIG.cardcount).forEach((cardId)
-> {
records.add(new CardEvent(cardId, amt, stationId));
}
);
return records;
}CREATE TOPIC RECHARGE execute procedure RechargeCard;
sqlcmd --query="load classes $PROJ_HOME/dist/procs.jar" CREATE PROCEDURE PARTITION ON TABLE cards COLUMN card_id PARAMETER 0 FROM CLASS metro.cards.RechargeCard;
public class RechargeCard extends VoltProcedure {
public final SQLStmt updateBalance = new SQLStmt("UPDATE cards SET balance = balance + ? WHERE card_id = ? AND card_type = 0");
public final SQLStmt getCard = new SQLStmt("SELECT * from cards WHERE card_id = ?");
public final SQLStmt exportNotif = new SQLStmt("INSERT INTO CARD_ALERT_EXPORT values (?, NOW, ?, ?, ?, ?, ?, ?)");
public final SQLStmt getStationName = new SQLStmt("SELECT name FROM stations WHERE station_id = ?");
public long run(int cardId, int amt, int stationId) {
voltQueueSQL(getStationName, stationId);
voltQueueSQL(getCard, cardId);
String station = "UNKNOWN";
final VoltTable[] results = voltExecuteSQL();
if(results.length == 0)
exportError(cardId, station);
VoltTable stationResult = results[0];
if(stationResult.advanceRow())
station = stationResult.getString(0);
VoltTable card = results[1];
if(card.advanceRow()) {
voltQueueSQL(updateBalance, amt, cardId);
String name = card.getString(5);
String phone = card.getString(6);
String email = card.getString(7);
int notify = (int) card.getLong(8);
voltQueueSQL(updateBalance, amt, cardId);
voltQueueSQL(exportNotif, cardId, station, name, phone, email, notify, "Card recharged successfully");
voltExecuteSQL(true);
} else {
exportError(cardId, station);
}
return 0;
}
private void exportError(int cardId, String station) {
exportError(cardId, station, "", "", "", 0, "Could not locate details of card for recharge");
}
private void exportError(int cardId, String station, String name, String phone, String email, int notify, String msg) {
voltQueueSQL(exportNotif, cardId, station, name, phone, email, notify, msg);
voltExecuteSQL(true);
}
}public final SQLStmt exportNotif = new SQLStmt("INSERT INTO CARD_ALERT_EXPORT values (?, NOW, ?, ?, ?, ?, ?, ?)");CREATE VIEW card_export_stats(card_id, station_name, rechargeCount) AS SELECT card_id, station_name, count(*) from CARD_ALERT_EXPORT GROUP BY card_id, station_name;
CREATE TOPIC using stream CARD_ALERT_EXPORT properties(Topic.format=avro);
void connectToOneServerWithRetry(String server, Client client) {
int sleep = 1000;
while (true) {
try {
client.createConnection(server);
break;
}
catch (Exception e) {
System.err.printf("Connection failed - retrying in %d second(s).\n", sleep / 1000);
try { Thread.sleep(sleep); } catch (Exception interruted) {}
if (sleep < 8000) sleep += sleep;
}
}
System.out.printf("Connected to VoltDB node at: %s.\n", server);
}private static final ScheduledExecutorService EXECUTOR = Executors.newScheduledThreadPool(100);
public void runBenchmark() throws Exception {
int microsPerTrans = 1000000/RidersProducer.config.rate;
EXECUTOR.scheduleAtFixedRate (
() -> {
List<Object[]> entryRecords = getEntryActivityRecords(config.cardcount);//生成随机的进站记录
call(config.cardEntry, entryRecords);//将数据发送到VoltDB数据库
}, 10000, microsPerTrans, MICROSECONDS);
}
public static List<Object[]> getEntryActivityRecords(int count) {
final ArrayList<Object[]> records = new ArrayList<>();
long curTime = System.currentTimeMillis();
ThreadLocalRandom.current().ints(1, 0, count).forEach((cardId)
-> {
records.add(new Object[] {cardId, curTime, Stations.getRandomStation().stationId, ENTER.value, 0});
}
);
return records;
} protected static void call(String proc, Object[] args) {
try {
client.callProcedure(new BenchmarkCallback(proc, args), procName, args);
} catch (IOException e) {
e.printStackTrace();
}
}@Option(desc = "Proc for card entry swipes") String cardEntry = "ValidateEntry";
//查询公交卡是否存在 public final SQLStmt checkCard = new SQLStmt( "SELECT enabled, card_type, balance, expires, name, phone, email, notify FROM cards WHERE card_id = ?;"); //卡充值 public final SQLStmt chargeCard = new SQLStmt( "UPDATE cards SET balance = ? WHERE card_id = ?;"); //查询指定站点的入站费用 public final SQLStmt checkStationFare = new SQLStmt( "SELECT fare, name FROM stations WHERE station_id = ?;"); //记录进站事件 public final SQLStmt insertActivity = new SQLStmt( "INSERT INTO card_events (card_id, date_time, station_id, activity_code, amount, accept) VALUES (?,?,?,?,?,?);"); //再次用到card_alert_export 这个stream对象,用于发送公交卡欠费消息 public final SQLStmt exportActivity = new SQLStmt( "INSERT INTO card_alert_export (card_id, export_time, station_name, name, phone, email, notify, alert_message) VALUES (?,?,?,?,?,?,?,?);"); //将刷卡欺诈行为写入stream对象fraud中 public final SQLStmt publishFraud = new SQLStmt( "INSERT INTO fraud (trans_id, card_id, date_time, station, activity_type, amt) values (?, ?, ?, ?, ?, ?)" );
CREATE STREAM FRAUD partition on column CARD_ID ( TRANS_ID varchar not null, CARD_ID integer not null, DATE_TIME timestamp not null, STATION integer not null, ACTIVITY_TYPE TINYINT not null, AMT integer not null ); create Topic using stream FRAUD properties(Topic.format=avro,consumer.keys=TRANS_ID);
public VoltTable run(int cardId, long tsl, int stationId, byte activity_code, int amt) throws VoltAbortException {
//查询公交卡是否存在
voltQueueSQL(checkCard, EXPECT_ZERO_OR_ONE_ROW, cardId);
//查询指定站点的交通费用
voltQueueSQL(checkStationFare, EXPECT_ONE_ROW, stationId);
VoltTable[] checks = voltExecuteSQL();
VoltTable cardInfo = checks[0];
VoltTable stationInfo = checks[1];
byte accepted = 0;
//如果公交卡记录等于0,说明卡不存在
if (cardInfo.getRowCount() == 0) {
//记录刷卡行为到数据库表中,将accept字段置为拒绝“REJECTED”
voltQueueSQL(insertActivity, cardId, tsl, stationId, ACTIVITY_ENTER, amt, ACTIVITY_REJECTED);
voltExecuteSQL(true);
//返回“被拒绝”消息给客户端。
return buildResult(accepted,"Card Invalid");
}
// 如果卡存在,则取出卡信息。
cardInfo.advanceRow();
//卡状态,0不可用,1可用
int enabled = (int)cardInfo.getLong(0);
int cardType = (int)cardInfo.getLong(1);
//卡余额
int balance = (int)cardInfo.getLong(2);
TimestampType expires = cardInfo.getTimestampAsTimestamp(3);
String owner = cardInfo.getString(4);
String phone = cardInfo.getString(5);
String email = cardInfo.getString(6);
int notify = (int)cardInfo.getLong(7);
// 查询指定站点的进站费用
stationInfo.advanceRow();
//指定站点的进站费用
int fare = (int)stationInfo.getLong(0);
String stationName = stationInfo.getString(1);
// 刷卡时间
TimestampType ts = new TimestampType(tsl);
// 如果卡状态为不可用
if (enabled == 0) {
//向客户端返回“此卡不可用”
return buildResult(accepted,"Card Disabled");
}
// 如果卡类型为“非月卡”
if (cardType == 0) { // 如果卡内余额充足
if (balance > fare) {
//isFrand为反欺诈策略,后面介绍
if (isFraud(cardId, ts, stationId)) {
// 如果认定为欺诈,记录刷卡记录,记录类型为“欺诈刷卡”
voltQueueSQL(insertActivity, cardId, ts, stationId, ACTIVITY_ENTER, fare, ACTIVITY_FRAUD);
//并且把欺诈事件写入stream,并最终被发布到VoltDB Topic中。见前面STREAM FRAUD到ddl定义
voltQueueSQL(publishFraud, generateId(cardId, tsl), cardId, ts, stationId, ACTIVITY_ENTER, amt);
voltExecuteSQL(true);
//向客户端返回“欺诈交易”消息
return buildResult(0, "Fraudulent transaction");
} else {
// 如果不是欺诈行为,则减少卡内余额,完成正常消费
voltQueueSQL(chargeCard, balance - fare, cardId);
//记录正常的刷卡事件
voltQueueSQL(insertActivity, cardId, ts, stationId, ACTIVITY_ENTER, fare, ACTIVITY_ACCEPTED);
voltExecuteSQL(true);
//向客户端返回卡内余额
return buildResult(1, "Remaining Balance: " + intToCurrency(balance - fare));
}
} else {
// 如果卡内余额不足,记录刷卡失败事件。
voltQueueSQL(insertActivity, cardId, ts, stationId, ACTIVITY_ENTER, 0, ACTIVITY_REJECTED);
if (notify != 0) {
//再次用到card_alert_export 这个stream对象,用于发送公交卡欠费消息
voltQueueSQL(exportActivity, cardId, getTransactionTime().getTime(), stationName, owner, phone, email, notify, "Insufficient Balance");
}
voltExecuteSQL(true);
//向客户端返回“余额不足“消息
return buildResult(0,"Card has insufficient balance: "+intToCurrency(balance));
}
}
}isFraud方法根据当前刷卡记录中的数据,结合数据库中的历史记录实现以上反欺诈规则。历史刷卡记录被保存在card_events表中,另外基于这张表创建了视图,统计每张卡在一秒钟内是否有过刷卡记录。
CREATE VIEW CARD_HISTORY_SECOND as select card_id, TRUNCATE(SECOND, date_time) scnd from card_events group by card_id, scnd;
isFraud方法的定义
public final SQLStmt cardHistoryAtStations = new SQLStmt(
"SELECT activity_code, COUNT(DISTINCT station_id) AS stations " +
"FROM card_events " +
"WHERE card_id = ? AND date_time >= DATEADD(HOUR, -1, ?) " +
"GROUP BY activity_code;"
);
public final SQLStmt cardEntries = new SQLStmt(
"SELECT activity_code " +
"FROM card_events " +
"WHERE card_id = ? AND station_id = ? AND date_time >= DATEADD(HOUR, -1, ?) " +
"ORDER BY date_time;"
);
public final SQLStmt instantaneousCardActivity = new SQLStmt(
"SELECT count(*) as activity_count "
+ "FROM CARD_HISTORY_SECOND "
+ "WHERE card_id = ? "
+ "AND scnd = TRUNCATE(SECOND, ?) "
+ "GROUP BY scnd;"
);
public boolean isFraud(int cardId, TimestampType ts, int stationId) {
voltQueueSQL(instantaneousCardActivity, cardId, ts);
voltQueueSQL(cardHistoryAtStations, cardId, ts);
voltQueueSQL(cardEntries, cardId, stationId, ts);
final VoltTable[] results = voltExecuteSQL();
final VoltTable cardInstantaneousActivity = results[0];
final VoltTable cardHistoryAtStationisTable = results[1];
final VoltTable cardEntriesTable = results[2];
//一秒钟之内已经有一次刷卡记录的话,返回true
while (cardInstantaneousActivity.advanceRow()) {
if(cardInstantaneousActivity.getLong("activity_count") > 0) {
return true;
}
}
while (cardHistoryAtStationisTable.advanceRow()) {
final byte activity_code = (byte) cardHistoryAtStationisTable.getLong("activity_code");
final long stations = cardHistoryAtStationisTable.getLong("stations");
if (activity_code == ACTIVITY_ENTER) {
// 过去1小时之内在五个站点刷卡进站,返回true
if (stations >= 5) {
return true;
}
}
}
byte prevActivity = ACTIVITY_INVALID;
int entranceCount = 0;
while (cardEntriesTable.advanceRow()) {
final byte activity_code = (byte) cardHistoryAtStationisTable.getLong("activity_code");
if (prevActivity == ACTIVITY_INVALID || prevActivity == activity_code) {
if (activity_code == ACTIVITY_ENTER) {
prevActivity = activity_code;
entranceCount++;
} else {
prevActivity = ACTIVITY_INVALID;
}
}
}
// 如果在过去1小时内有10次连续的刷卡记录,返回true。
if (entranceCount >= 10) {
return true;
}
return false;
}
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/69903219/viewspace-2768059/ ,如需转载,请注明出处,否则将追究法律责任。