feat(携号转网): 实现携号转网接口并集成到充值系统

- 新增 Baidumnpapi 类处理百度携号转网 API
- 在 Core 中添加携号转网相关的逻辑和接口
- 修改设置页面,增加携号转网配置选项- 新增商户余额监控和预警功能
- 优化订单处理流程,支持携号转网
This commit is contained in:
mzeros 2024-12-22 17:41:42 +08:00
parent 20a9af6fd0
commit 0cfae47bab
8 changed files with 813 additions and 1 deletions

View File

@ -0,0 +1,116 @@
<?php
namespace app\channel\service\system;
/**
* 百度携号转网api
* Class Baidumnpapi
* @package app\channel\service
*/
class Baidumnpapi extends Core
{
# 这几个可以后台设置
protected $host = '';
protected $mid = '';
protected $token = '';
protected $method = '';
#通用充值
public function phone_mnp($mobile)
{
$url = $this->host .'?mobile='.$mobile;
$headers = [
'Content-Type'=>'application/json',
'X-Bce-Signature'=>'AppCode/'.$this->mid,
];
$response = $this->curl('post', $url, [] ,true,$headers);
$response = $this->response($response);
//$response['data'] = '{}';
//$response['msg'] = 'ok';
if($response['msg'] == 'ok'){
if(isset($response['array']['result']['Now_isp'])){
if($response['array']['result']['Now_isp'] == '移动'){
return 1;
}elseif ($response['array']['result']['Now_isp'] == '联通'){
return 2;
}else{
return 3;
}
}
}
return $response['msg'];
}
# 数据响应格式处理
public function response($data)
{
$log['type'] = 'response';
$log['data'] = $data;
$log['config'] = $this->data;
$this->log($log);
$array = $this->json_decode($data);
if (!$array) {
$msg = 'error';
} elseif (isset($array['code']) && $array['code'] == 0) {
# 正确
$msg = 'ok';
} else {
# 错误
$msg = isset($array['info']) ? $array['info'] : 'error';
}
return array
(
'msg' => $msg,
'data' => $data,
'array' => $array,
);
}
# 查询余额接口
public function account($day)
{
// $request['day'] = $day;
// $request['appid'] = $this->mid;
// $request['version'] = 'v4';
// $request['product'] = 'query';
// list($msec, $sec) = explode(' ', microtime());
// $msectime = (float)sprintf('%.0f', (floatval($msec) + floatval($sec)) * 1000);
// $request['time'] = $msectime;
// $request['nonce'] = substr(sha1(microtime()), rand(10, 15));
// $request['sign'] = $this->_sign($request);
// $url = $this->host . 'handle/accountByDay';
// $response = $this->curl('post', $url, $request,true);
// $response = $this->response($response);
//
// return $response['array']['data'];
}
# 提交数据
private function submit($param, $check)
{
}
}

View File

@ -2,6 +2,8 @@
namespace app\gateway\controller\v4; namespace app\gateway\controller\v4;
use app\core\Service;
use app\merchant\service\MerchantLogService;
use app\merchant\service\OrderLastHistoryService; use app\merchant\service\OrderLastHistoryService;
use app\merchant\service\OrderLastweekHistoryService; use app\merchant\service\OrderLastweekHistoryService;
use app\merchant\service\OrderTwoHistoryService; use app\merchant\service\OrderTwoHistoryService;
@ -138,12 +140,18 @@ class Core extends Controller
} }
if (!$this->product) { if (!$this->product) {
$this->no(-2); $this->no(-2);
} }
# 从数据库中或者缓存中取出商户的信息,并验证是否有效 # 从数据库中或者缓存中取出商户的信息,并验证是否有效
$this->getMerchant(); $this->getMerchant();
if($this->product == 'dhcz_mnp'){
$this->getMnp_isp($this->input['mobile']);
}
#验证安全进价是否匹配 #验证安全进价是否匹配
if($this->product && $this->product != 'query' && $this->product != '' && isset($this->input['safe_price'])){ if($this->product && $this->product != 'query' && $this->product != '' && isset($this->input['safe_price'])){
@ -228,6 +236,74 @@ class Core extends Controller
} }
return $this->request->ip(); // 如果没有X-Forwarded-For头则返回当前请求的IP return $this->request->ip(); // 如果没有X-Forwarded-For头则返回当前请求的IP
}
#检测运营商并更新运营商
public function getMnp_isp($mobile)
{
$settingPhoneMNPStatus = sysconf('settingPhoneMNPStatus');
if ($settingPhoneMNPStatus == 1) {
$mnp_cid = sysconf('PhoneMnp_cid');
if($mnp_cid){
$mnp_isp = ChannelService::instance()->call('phone_mnp', $mnp_cid, $mobile);
if($mnp_isp == 1){
$this->product = 'ydcz';
$this->input['mnp_isp'] = '移动';
$this->Kou_Mnp($mobile);
return $this->product ;
}elseif($mnp_isp == 2){
$this->product = 'ltcz';
$this->input['mnp_isp'] = '联通';
$this->Kou_Mnp($mobile);
return $this->product ;
}elseif($mnp_isp == 3){
$this->product = 'dxcz';
$this->input['mnp_isp'] = '电信';
$this->Kou_Mnp($mobile);
return $this->product ;
}
}
}
$isp = Service::instance()->isp($this->input['mobile']);
if ($isp == 1) {
$this->product = 'ydcz';
} elseif ($isp == 2) {
$this->product = 'ltcz';
} elseif ($isp == 3) {
$this->product = 'dxcz';
}
return $this->product ;
}
public function Kou_Mnp($mobile)
{
$new_cash = 0.002;
if ($this->merchant['account_surplus'] >= $new_cash) {
$account_type = 1;
} elseif ($this->merchant['credit_surplus'] >= $new_cash) {
$account_type = 2;
}
$redis = RedisService::getInstance();
// 记录扣费到 Redis
// 根据 this->mid 分别记录不同用户的调用次数
$redis_key = 'merchant_cash_deduction:' . $this->mid;
$deduction_count = $redis->incr($redis_key,1);
if ($deduction_count % 10 == 0) {
$new_cash = $new_cash*10;
MerchantLogService::instance()->add($this->mid, $new_cash, $account_type, 3, 'API扣费: 业务余额扣除 - 10次调用携号转网');
// 重置 Redis 中的调用次数
$redis->set($redis_key, 0);
}
} }
# 检测开放或者维护时间 # 检测开放或者维护时间

View File

@ -3,6 +3,9 @@
namespace app\private_api\controller\merchantApi; namespace app\private_api\controller\merchantApi;
use app\channel\service\ChannelService; use app\channel\service\ChannelService;
use app\core\Service;
use app\gateway\service\RedisService;
use app\merchant\service\MerchantLogService;
/** /**
* 接口处理 * 接口处理
@ -26,11 +29,21 @@ class Order extends Core
$this->getMerchant(); $this->getMerchant();
$param['cash'] = $this->input['cash']; $param['cash'] = $this->input['cash'];
$param['mobile'] = $this->input['account']; $param['mobile'] = $this->input['account'];
$param['product'] = $this->input['product_key'];
if($this->product == 'dhcz_mnp'){
$this->getMnp_isp($this->input['account']);
if(isset($this->input['mnp_isp'])){
$param['mnp_isp'] = $this->input['mnp_isp'];
}
}
$param['product'] =$this->product;
$param['order'] = $this->input['order_no']; $param['order'] = $this->input['order_no'];
@ -39,6 +52,77 @@ class Order extends Core
} }
#检测运营商并更新运营商
public function getMnp_isp($mobile)
{
$settingPhoneMNPStatus = sysconf('settingPhoneMNPStatus');
if ($settingPhoneMNPStatus == 1) {
$mnp_cid = sysconf('PhoneMnp_cid');
if($mnp_cid){
$mnp_isp = ChannelService::instance()->call('phone_mnp', $mnp_cid, $mobile);
if($mnp_isp == 1){
$this->product = 'ydcz';
$this->input['mnp_isp'] = '移动';
$this->Kou_Mnp($mobile);
return $this->product ;
}elseif($mnp_isp == 2){
$this->product = 'ltcz';
$this->input['mnp_isp'] = '联通';
$this->Kou_Mnp($mobile);
return $this->product ;
}elseif($mnp_isp == 3){
$this->product = 'dxcz';
$this->input['mnp_isp'] = '电信';
$this->Kou_Mnp($mobile);
return $this->product ;
}
}
}
$isp = Service::instance()->isp($this->input['mobile']);
if ($isp == 1) {
$this->product = 'ydcz';
} elseif ($isp == 2) {
$this->product = 'ltcz';
} elseif ($isp == 3) {
$this->product = 'dxcz';
}
return $this->product ;
}
public function Kou_Mnp($mobile)
{
$new_cash = 0.002;
if ($this->merchant['account_surplus'] >= $new_cash) {
$account_type = 1;
} elseif ($this->merchant['credit_surplus'] >= $new_cash) {
$account_type = 2;
}
$redis = RedisService::getInstance();
// 记录扣费到 Redis
// 根据 this->mid 分别记录不同用户的调用次数
$redis_key = 'merchant_cash_deduction:' . $this->mid;
$deduction_count = $redis->incr($redis_key,1);
if ($deduction_count % 10 == 0) {
$new_cash = $new_cash*10;
MerchantLogService::instance()->add($this->mid, $new_cash, $account_type, 3, 'API扣费: 业务余额扣除 - 10次调用携号转网');
// 重置 Redis 中的调用次数
$redis->set($redis_key, 0);
}
}
protected function maketime($v): float|false|int|string protected function maketime($v): float|false|int|string
{ {
if (!$v) { if (!$v) {

View File

@ -0,0 +1,279 @@
<?php
namespace app\private_api\controller\monitorApi;
use app\gateway\service\RedisService;
use app\merchant\service\MerchantService;
use dever\Log;
use think\admin\Controller;
/**
* 接口核心控制器
* Class Core
* @package app\gateway\service
*/
class Core extends Controller
{
# 是否检测数据
protected bool $check = true;
# 商户appid
protected string $appid = '';
# 商户id
protected $mid = 0;
# 商户信息
protected array $merchant = array();
# token key
protected string $key = '';
# product key
protected string $product = 'query';
# proid
protected $proid = 0;
# input
protected array $input = array();
# use_product
protected array $use_product = array();
# sign_type
protected $sign_type = 1;
protected string $request_ip = '';
# code码定义
protected array $code = array
(
# 成功
1 => 'ok',
# 小于0为失败
-1 => 'mid为空',
-2 => '产品错误',
-3 => 'appid无效',
-4 => '余额不足或者没有传入价格',
-5 => 'sign不能为空',
-6 => 'nonce不能为空',
-7 => 'time不能为空',
-8 => 'sign已失效',
-9 => 'sign验证失败',
-10 => '参数错误',
-100 => '请求错误',
-101 => '订单不存在',
-102 => '订单号重复',
-103 => '请求错误,请求格式不正确请使用json请求',
-104 => '回调渠道订单号不匹配,请核对',
-201 => '安全进价低于实际进价',
-801 => 'ip: 请求错误,不在白名单',
-802 => '方法不存在',
-803 => '版本号不正确',
-1000 => '系统维护中',
);
#找不到方法错误响应
public function __call($name, $arguments)
{
$this->error('请求' . $name . '错误', '{-null-}', -802);
}
public function initialize(): void
{
parent::initialize();
$this->sign_type = sysconf('sign_type');
if ($this->check) {
$this->check();
}
}
# 获取输入的信息
public function input(): void
{
$this->input = input();
if (!$this->input) {
$this->no(-100);
}
}
/**
* 检测输入信息是否合法
*/
protected function check()
{
if (!str_contains($this->request->header('content_type'), "application/json")) $this->no(-103);
// $this->input();
$input = input();
if (!$input) {
$this->no(-100);
}
$ip_white_data = sysconf('MerchantSystem_ip_white');
$request_ip = $this->getRealIp();
// if()
$ip_white_array = $ip_white_data ? explode(',', $ip_white_data) : [];
if (!$ip_white_array || !in_array($request_ip, $ip_white_array)) {
$this->no(-801, 'ip:' . $request_ip . ',请求错误,不在白名单内。');
}
$old_sign = $input['sign'];
unset($input['sign']);
#版本签名
$signature = $this->sign($input, $this->merchant['token'], $request_ip);
if($signature != $old_sign){
$this->no(-9, 'sign验证失败');
}
}
public function sign($dta,$token,$ip)
{
ksort($dta);
$signature_string = '';
foreach ($dta as $k => $v) {
$signature_string .= $k . '=' . $v . '&';
}
$tokens = md5($token.'||'.$ip);
$signature_string .= 'key='.$tokens;
#此处采用ip+token进行签名
return md5($signature_string);
}
public function getRealIp(): string
{
$forwardedFor = $this->request->header('x-forwarded-for');
if ($forwardedFor) {
// 可能有多个IP第一个通常是真实的
$ips = explode(',', $forwardedFor);
return trim($ips[0]);
}
return $this->request->ip(); // 如果没有X-Forwarded-For头则返回当前请求的IP
}
# 检测开放或者维护时间
protected function checkOpenTime($opentime): void
{
if ($opentime && strstr($opentime, ':')) {
$opentime = str_replace(':', '', $opentime);
if (str_contains($opentime, '-')) {
$value = explode('-', $opentime);
$cur = intval(date('Hi'));
$value[0] = intval($value[0]);
$value[1] = intval($value[1]);
if ($value[1] < $value[0]) {
if ($cur >= $value[0] || $cur < $value[1]) {
$this->no(-1000);
}
} else {
if ($cur >= $value[0] && $cur < $value[1]) {
$this->no(-1000);
}
}
}
}
}
# 获取商户信息
protected function getMerchant(): void
{
$this->merchant = MerchantService::instance()->get($this->mid);
if (!$this->merchant) {
$this->no(-3);
}
if ($this->product != 'query') {
if (empty($this->input['cash'])) {
$this->no(-4);
}
if ($this->product == 'dxdc') {
$cash = 0.23;//这里后续处理
if ($this->merchant['credit_surplus'] < $cash && $this->merchant['account_surplus'] < $cash) {
$this->no(-4);
}
} else {
if ($this->merchant['credit_surplus'] < $this->input['cash'] && $this->merchant['account_surplus'] < $this->input['cash']) {
$this->no(-4);
}
}
}
$this->mid = $this->merchant['id'];
if ($this->sign_type == 2) {
$this->key = $this->merchant['appsecret'];
} else {
$this->key = $this->appid . '|' . $this->merchant['appsecret'];
}
}
# 查找订单
/**
* 返回失败的消息
* @param mixed $info
* @param string $data
* @param integer $code
*/
protected function no(int $code = 0, $info = '', $data = '{-null-}')
{
$msg = $this->code[$code] ?? 'error';
if ($info) {
$msg .= ':' . $info;
}
$data = '{-null-}';
$this->error($msg, $data, $code);
}
/**
* 返回成功的消息
* @param mixed $info
* @param string $data
* @param integer $code
*/
protected function yes($data = '{-null-}', $info = 'ok', $code = 1)
{
if (is_string($data) && $data != 'ok' && $data != 'success') {
if ($data == '订单号重复') {
return $this->no(-102);
}
return $this->no(-100, $data);
}
$this->success($info, $data, $code);
}
# 记录日志
protected function log($data)
{
Log::write('private_api', 'merchantApi', $data);
}
# 队列
protected function queue($key, $value = false)
{
$redis = RedisService::getInstance();
if (!$value) {
return $redis->pop($key);
} else {
return $redis->push($key, $value);
}
}
}

View File

@ -0,0 +1,85 @@
<?php
namespace app\private_api\controller\monitorApi;
use app\merchant\service\MerchantService;
/**
* 接口处理
* Class Handle
* @package app\gateway\api
*/
class Merchant extends Core
{
# 是否检测数据
protected bool $check = false;
#获取商户余额
public function AllBalance()
{
$data = MerchantService::instance()->getAll();
$where['is_deleted'] = 0;
$where['status'] = 1;
$data = MerchantService::instance()->db()->field('name,account_surplus,credit_surplus,appid')->where($where)->select()->toArray();
$this->yes($data);
}
protected function maketime($v): float|false|int|string
{
if (!$v) {
return '';
}
if (is_numeric($v)) {
return $v;
}
if (is_array($v)) {
$v = $v[1];
}
if (strstr($v, ' ')) {
$t = explode(' ', $v);
$v = $t[0];
$s = explode(':', $t[1]);
} else {
$s = array(0, 0, 0);
}
if (!isset($s[1])) {
$s[1] = 0;
}
if (!isset($s[2])) {
$s[2] = 0;
}
if (strstr($v, '-')) {
$t = explode('-', $v);
} elseif (strstr($v, '/')) {
$u = explode('/', $v);
$t[0] = $u[2];
$t[1] = $u[0];
$t[2] = $u[1];
}
if (!isset($t)) {
$t = array(0, 0, 0);
}
if (!isset($t[1])) {
$t[1] = 0;
}
if (!isset($t[2])) {
$t[2] = 0;
}
$v = mktime($s[0], $s[1], $s[2], $t[1], $t[2], $t[0]);
return $v;
}
}

View File

@ -85,6 +85,21 @@ class WeChatBot extends Core
// var_dump($data['sender']);die; // var_dump($data['sender']);die;
return $RobotService->send_text('测试自动回复 @'.$ac,$data['roomid'],$data['sender']); return $RobotService->send_text('测试自动回复 @'.$ac,$data['roomid'],$data['sender']);
} }
if($roomid == '47576792968@chatroom' && str_contains($data['xml'],'wxid_9iv1hha8g3ok29')){
$AllContacts = $RobotService->get_all_contacts();
// var_dump($AllContacts);die;
$Contacts_array = json_decode($AllContacts,true);
$acc = $Contacts_array['data']['contacts'];
// var_dump($acc);die;
$ac = '';
foreach ($acc as $k=>$v){
if($v['wxid'] == $data['sender']){
$ac = $v['name'];
}
}
// var_dump($data['sender']);die;
return $RobotService->send_text('测试自动回复 @'.$ac,$data['roomid'],$data['sender']);
}
$this->response(['code'=>1]); $this->response(['code'=>1]);
} }

View File

@ -0,0 +1,114 @@
<?php
namespace app\queue\command\monitor;
use app\channel\service\ChannelService;
use app\gateway\controller\api\Notify;
use app\gateway\service\RedisService;
use app\merchant\service\MerchantService;
use app\merchant\service\OrderService;
use think\admin\Command;
use think\Collection;
use think\console\Input;
use think\console\Output;
/**
* 商户余额监控预警
* Class MerchantBalanceMonitor
* @package app\data\command
*/
class MerchantBalanceMonitor extends Command
{
protected function configure()
{
$this->setName('xQueue:MerchantBalanceMonitor')->setDescription('[ 监控系统 ] 商户余额监控预警');
}
/**
* @param Input $input
* @param Output $output
* @throws \think\admin\Exception
*/
protected function execute(Input $input, Output $output)
{
ini_set('memory_limit', '1024M');
$merchantService = MerchantService::instance();
$merchantsList = $merchantService->db()->where(['merchant_type'=>1,'status'=>1])->select()->toArray();
// if(!$channelList) $this->setQueueSuccess("未找到支持查询的渠道");
$merchantsList_num = count($merchantsList);
$count_sum = 0;
$total_sum=0;
$redisStatus = sysconf('settingRedisQueryOrder');
foreach($merchantsList as $merchants) {
$class = OrderService::instance();
$where['cid'] = $channel['id'];
$where['status'] = 4;
if($redisStatus == 1){
#多线程此处只传入队列
$orderList = $class->db()->field('cid,order_id')->where($where)->order('id asc')->select()->toArray();
foreach ($orderList as $vo) {
$this->queue('query', $vo);
}
// $this->queue('query', $orderList);
}else{
list($count, $total) = [0, $class->db()->where($where)->order('id asc')->count()];
$total_sum += $total;
// $data = $class->db()->where($where)->limit(100)->select()->toArray();
$class->db()->where($where)->order('id asc')->chunk(1000, function (Collection $data) use (&$count, $total,$channel,$channelService,&$count_sum) {
$array = $data->toArray();
foreach ($array as $k => $vo) {
if(isset($vo['num']) && $vo['num']>0) {
$vo['order_id'] = $vo['order_id'].'_'.$vo['num'];
}
$result = $channelService->call('query', $vo['cid'], $vo);
if ($result['status'] != 4) {
$count_sum++;
$count++;
$notifyClass = new Notify($this->app);
$notifyClass->queue_query($vo['order_id'], $result);
$this->setQueueProgress("查询 {$vo['order_id']} 新的状态进行主动更新", $count / $total * 100);
}
}
});
}
}
$this->setQueueSuccess("共处理 {$channelList_num} 个渠道,共有 {$total_sum} 个订单处理查询中, 完成{$count_sum} 个订单查询新状态更新!");
}
# 队列
protected function queue($key, $value = false)
{
$redis = RedisService::getInstance();
if (!$value) {
return $redis->pop($key);
} else {
return $redis->push($key, $value);
}
}
}

View File

@ -47,6 +47,33 @@
<span class="help-block">请填写维护时间为空则24小时开启值为22:30-00:30</span> <span class="help-block">请填写维护时间为空则24小时开启值为22:30-00:30</span>
</label> </label>
<label class="layui-form-item margin-top-20 block relative">
<span class="color-green margin-right-10">是否支持携号转网接口</span>
<div>
{foreach ['1'=>'是','2'=>'否'] as $k=>$v}
<label class="think-radio">
{if sysconf('settingPhoneMNPStatus') eq $k}
<input checked type="radio" id="settingPhoneMNPStatus" name="settingPhoneMNPStatus" value="{$k}" lay-ignore> {$v}
{else}
<input type="radio" id="settingPhoneMNPStatus2" name="settingPhoneMNPStatus" value="{$k}" lay-ignore> {$v}
{/if}
</label>
{/foreach}
</div>
<span class="color-desc">开启后支持dhcz_mnp产品进行三网自动判断</span>
</label>
<div class="layui-form-item" id="Mnp_info" style="display: none;">
<label class="layui-form-item relative block" >
<span class="color-black font-w7">携转查询接口渠道ID</span>
<input class="layui-input" placeholder="请输入携转接口渠道ID" name="PhoneMnp_cid" value="{:sysconf('PhoneMnp_cid')}"/>
<span class="help-block">携转查询接口渠道ID</span>
</label>
</div>
<label class="layui-form-item margin-top-20 block relative"> <label class="layui-form-item margin-top-20 block relative">
<span class="color-green margin-right-10">是否支持宝塔Nginx WAF防火墙</span> <span class="color-green margin-right-10">是否支持宝塔Nginx WAF防火墙</span>
<div> <div>
@ -254,6 +281,12 @@
}else { }else {
$("#btWaf_info").hide() $("#btWaf_info").hide()
} }
var objs = document.getElementsByName("settingPhoneMNPStatus")
if(objs[0].checked){
$("#Mnp_info").show()
}else {
$("#Mnp_info").hide()
}
$(document).ready(function() { $(document).ready(function() {
@ -266,6 +299,16 @@
}); });
}); });
$(document).ready(function() {
$('input[type=radio][name=settingPhoneMNPStatus]').change(function() {
if (this.value == '1') {
$("#Mnp_info").show()
} else if (this.value == '2') {
$("#Mnp_info").hide()
}
});
});