作者存档
20251 月21

通过 certbot 自动部署ssl证书

Let`s Encrypt 证书自动获取和部署

sudo apt install certbot python3-certbot-nginx

sudo certbot --nginx

sudo systemctl reload nginx

sudo certbot renew --dry-run
20251 月20

dwg 文件转换为 shp 并发布为图层

1、ArgGIS 按图层分别导出shp

原始没有坐标系,要手动配准:

投影坐标 4524 CGCS2000_3_Degree_GK_Zone_36

2、GeoServer

1)创建工作区

2)数据存储-添加新的数据存储-选择文件夹 Directory of spatial files (shapefiles) 

3)图层-添加新的资源-选择要发布的文件-Native Bounding Box 从数据中计算

20251 月3

解决方法:个推推送离线消息到华为手机不显示问题(或只能收到2次消息)

问题1:只能收到APP在线时推送的消息,离线消息收不到

问题2:一台设备一天只能收到2次消息推送

解决方法:

1、uniapp 要正确获取clientId,并且APP要获取通知权限

bindPushCid() {	   
		   var timer = setTimeout(function() {
				plus.push.getClientInfoAsync(function(info) {
					if (info.clientid) {
						// 绑定到后台用户
						updateGetuiClientId(info.clientid);
						clearInterval(timer);
					}
				}, function(e) {
					console.log(JSON.stringify(e));
				})
			}, 1000)
		},
permissionPush(){
			let platform = uni.getSystemInfoSync().platform
			if (platform == 'android') {
				/* 获取当前手机是否有通知权限 */
				// let main = plus.android.runtimeMainActivity();
				// let pkName = main.getPackageName();
				// console.log("是否有通知权限pkName",pkName);
				// let NotificationManagerCompat = plus.android.importClass("android.support.v4.app.NotificationManagerCompat");
				// 	console.log("是否有通知权限NotificationManagerCompat",NotificationManagerCompat);
				// let packageNames = NotificationManagerCompat.from(main);
				
				var main = plus.android.runtimeMainActivity();
				var NotificationManagerCompat = plus.android.importClass("androidx.core.app.NotificationManagerCompat");
						let packageNames = NotificationManagerCompat.from(main);
				let pkName = main.getPackageName();
				if (!packageNames.areNotificationsEnabled()) { //手机没有开启通知的权限
					uni.showModal({
						title: '通知权限',
						content: '通知权限暂未开启',
						cancelText: '暂不开启',
						confirmText: '前往开启',
						 success: function (res) {
								if (res.confirm) {
									  let uid = main.getApplicationInfo().plusGetAttribute("uid");
									  let Intent = plus.android.importClass('android.content.Intent');
									  let Build = plus.android.importClass("android.os.Build");
									  let intent = '';
									  //android 8.0引导  
									  if (Build.VERSION.SDK_INT >= 26) {
										intent = new Intent('android.settings.APP_NOTIFICATION_SETTINGS');
										intent.putExtra('android.provider.extra.APP_PACKAGE', pkName);
									  } else if (Build.VERSION.SDK_INT >= 21) { //android 5.0-7.0  
										intent = new Intent('android.settings.APP_NOTIFICATION_SETTINGS');
										intent.putExtra("app_package", pkName);
										intent.putExtra("app_uid", uid);
									  } else { //(<21)其他--跳转到该应用管理的详情页
										let Settings = plus.android.importClass("android.provider.Settings");
										let Uri = plus.android.importClass("android.net.Uri");
										intent = new Intent();
										intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
										let uri = Uri.fromParts("package", main.getPackageName(), null);
										intent.setData(uri);
									  }
									  // 跳转到该应用的系统通知设置页  
									  main.startActivity(intent);
				
								} else if (res.cancel) {
									console.log('用户点击取消');
								}
							},
						fail: () => {},
						complete: () => {}
					});
				}
			}
		}

2、华为开发者后台,申请“自分类权益”,以便解除通知条数限制,现在申请很简单,提交示例和截图,系统直接审核通过

3、厂家参数要注意细节

因为importance和category参数,调试很久

        Map<String, Map<String, Object>> options = new HashMap<>();
        Map<String, Object> oop = new HashMap<>();
        oop.put("/message/android/notification/badge/class", "io.dcloud.PandoraEntry");
        oop.put("/message/android/notification/badge/add_num", 1);
        oop.put("/message/android/notification/importance", "NORMAL");
        oop.put("/message/android/category", "WORK");
        options.put("HW", oop);
        ups.setOptions(options);

4、服务端完整代码

package com.budwk.app;

import com.getui.push.v2.sdk.ApiHelper;
import com.getui.push.v2.sdk.GtApiConfiguration;
import com.getui.push.v2.sdk.api.PushApi;
import com.getui.push.v2.sdk.common.ApiResult;
import com.getui.push.v2.sdk.dto.CommonEnum;
import com.getui.push.v2.sdk.dto.req.Audience;
import com.getui.push.v2.sdk.dto.req.AudienceDTO;
import com.getui.push.v2.sdk.dto.req.message.PushChannel;
import com.getui.push.v2.sdk.dto.req.message.PushDTO;
import com.getui.push.v2.sdk.dto.req.message.PushMessage;
import com.getui.push.v2.sdk.dto.req.message.android.AndroidDTO;
import com.getui.push.v2.sdk.dto.req.message.android.GTNotification;
import com.getui.push.v2.sdk.dto.req.message.android.ThirdNotification;
import com.getui.push.v2.sdk.dto.req.message.android.Ups;
import com.getui.push.v2.sdk.dto.req.message.harmony.HarmonyDTO;
import com.getui.push.v2.sdk.dto.req.message.harmony.HarmonyNotification;
import com.getui.push.v2.sdk.dto.res.TaskIdDTO;
import com.gexin.rp.sdk.base.IPushResult;
import com.gexin.rp.sdk.base.impl.AppMessage;
import com.gexin.rp.sdk.base.impl.ListMessage;
import com.gexin.rp.sdk.base.impl.SingleMessage;
import com.gexin.rp.sdk.base.impl.Target;
import com.gexin.rp.sdk.exceptions.RequestException;
import com.gexin.rp.sdk.http.IGtPush;
import com.gexin.rp.sdk.template.*;
import com.gexin.rp.sdk.template.style.Style0;
import lombok.extern.slf4j.Slf4j;
import org.nutz.json.Json;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Slf4j
public class GeTuiUtil {


    private static String appId;
    private static String appKey;
    private static String masterSecret;
    private static String host;
    private static Long OfflineExpireTime;
    private static String logo;
    private static boolean isRing;
    private static boolean isVibrate;
    private static boolean isClearable;
    private static int transmissionType;

    private static PushApi pushApi;

    // 初始化个推的系统app参数
    static {
        appId = "";
        appKey = "";
        masterSecret = "";
        host = "http://sdk.open.api.igexin.com/apiex.htm";
        OfflineExpireTime = 259200000L;
        logo = "icon.png";
        isRing = true;
        isVibrate = true;
        isClearable =true;
        transmissionType = 1;

        System.setProperty("http.maxConnections", "200");
        GtApiConfiguration apiConfiguration = new GtApiConfiguration();
        //填写应用配置
        apiConfiguration.setAppId(appId);
        apiConfiguration.setAppKey(appKey);
        apiConfiguration.setMasterSecret(masterSecret);
        // 接口调用前缀,请查看文档: 接口调用规范 -> 接口前缀
        apiConfiguration.setDomain("https://restapi.getui.com/v2/");
        // 实例化ApiHelper对象,用于创建接口对象
        ApiHelper apiHelper = ApiHelper.build(apiConfiguration);
        // 创建对象,建议复用。目前有PushApi、StatisticApi、UserApi
        pushApi = apiHelper.creatApi(PushApi.class);

    }

    public static Map<String, Object> sendSingleNew(String title, String content, String cid) {
        PushDTO<Audience> pushDTO = new PushDTO<Audience>();
        pushDTO.setRequestId(System.currentTimeMillis() + "");

        buildPushMessage(title, content, pushDTO);

        Audience audience = new Audience();
        pushDTO.setAudience(audience);
        audience.addCid(cid);
        ApiResult<Map<String, Map<String, String>>> apiResult = pushApi.pushToSingleByCid(pushDTO);
        log.info("sendSingleNew result :" + Json.toJson(apiResult));
        Map<String, Object> result = new HashMap<>();
        result.put("code", apiResult.getCode());
        if (apiResult.isSuccess()) {
            result.put("data", apiResult.getData());
        } else {
            result.put("msg", apiResult.getMsg());
        }
        return result;
    }

    public static void main(String[] args) {
        String title = "你有新的待办任务";
        String content = "任务内容:测" + System.currentTimeMillis();
        String cid = "77336b3d6d136da0cf56e288a7462949";
        Map<String, Object> stringObjectMap = sendSingleNew(title, content, cid);

//        List<String> cids = new ArrayList<>();
//        cids.add(cid);
//        Map<String, Object> stringObjectMap = sendBatchNew(title, content, cids);

//        Map<String, Object> stringObjectMap = sendAllClientNew(title, content);
//
    }


    private static PushMessage buildPushMessage(String title, String content, PushDTO pushDTO) {

        PushMessage pushMessage = new PushMessage();
        pushDTO.setPushMessage(pushMessage);
        GTNotification notification = new GTNotification();
        pushMessage.setNotification(notification);
        notification.setTitle(title);
        notification.setBody(content);
        notification.setBadgeAddNum("1");
        notification.setClickType(CommonEnum.ClickTypeEnum.TYPE_STARTAPP.type);
        notification.setSlotType("2");
        notification.setCategory("CATEGORY_REMINDER");

        PushChannel pushChannel = new PushChannel();
        pushDTO.setPushChannel(pushChannel);

        AndroidDTO androidDTO = new AndroidDTO();
        pushChannel.setAndroid(androidDTO);
        Ups ups = new Ups();
        androidDTO.setUps(ups);
        ThirdNotification thirdNotification = new ThirdNotification();
        ups.setNotification(thirdNotification);
        thirdNotification.setTitle(title);
        thirdNotification.setBody(content);
        thirdNotification.setClickType(CommonEnum.ClickTypeEnum.TYPE_STARTAPP.type);

        Map<String, Map<String, Object>> options = new HashMap<>();
        Map<String, Object> oop = new HashMap<>();
        oop.put("/message/android/notification/badge/class", "io.dcloud.PandoraEntry");
        oop.put("/message/android/notification/badge/add_num", 1);
        oop.put("/message/android/notification/importance", "NORMAL");
        oop.put("/message/android/category", "WORK");
        options.put("HW", oop);
        ups.setOptions(options);
        
        
        HarmonyDTO harmonyDTO = new HarmonyDTO();
        pushChannel.setHarmony(harmonyDTO);
        HarmonyNotification harmonyNotification = new HarmonyNotification();
        harmonyDTO.setNotification(harmonyNotification);
        harmonyNotification.setTitle(title);
        harmonyNotification.setBody(content);
        harmonyNotification.setCategory("CATEGORY_REMINDER");
        harmonyNotification.setClickType(CommonEnum.ClickTypeEnum.TYPE_STARTAPP.type);
        return pushMessage;
    }


    private static Map<String, Object> send(AbstractTemplate template, List<String> cids) {
        if (cids == null || cids.size() == 0) {
            // 给appId下所有人发
            return sendAllClient(template);
        }
        if (cids.size() == 1) {
            // 单发
            Map<String, Object> result = sendSingle(template, cids.get(0));
            return result;
        } else {
            // 群发
            Map<String, Object> result = sendBatch(template, cids);
            return result;
        }
    }

    private static Map<String, Object> sendNew(String title, String content, List<String> cids) {
        log.info("进入推送信息:" + title);
        if (cids == null || cids.size() == 0) {
            // 给appId下所有人发
            Map<String, Object> stringObjectMap = sendAllClientNew(title, content);
            log.info("APP群推发送返回结果:" + Json.toJson(stringObjectMap));
            return stringObjectMap;
        }
        if (cids.size() == 1) {
            // 单发
            Map<String, Object> result = sendSingleNew(title, content, cids.get(0));
            log.info("单推发送返回结果:" + Json.toJson(result));
            return result;
        } else {
            // 群发
            Map<String, Object> result = sendBatchNew(title, content, cids);
            log.info("群推发送返回结果:" + Json.toJson(result));
            return result;
        }
    }

    /**
     * @Description 发送通知模板消息
     * @Author daitao
     * @version 1.0
     * @Date 2019/4/24 17:57
     * @Param title:消息标题
     * @Param content:消息内容
     * @Param cids: 1、null或size==0:表示给appId下所有人发
     * 2、size为1:单发
     * 3、size大于1:群发
     * @Return map类型, 可能为null,例如:"{result=ok, contentId=OSL-0424_y2LxEeM6hvA0yhTqa77qw4,
     * details={"c85fa1218fe4c54652a77bef22726fb0":"TokenMD5Error",
     * "a5800ff27659a5258b8ba86e4e1d7c87":"successed_online"
     * }
     * }"
     * result==ok,发送成功,details里面是每个cid对应的发送结果,包含successed表示成功,其他失败
     */
    public static Map<String, Object> sendNotification(String title, String content, List<String> cids) {
        NotificationTemplate template = buildNotificationTemplate(title, content);
        return send(template, cids);
    }

    public static Map<String, Object> sendNotificationNew(String title, String content, List<String> cids) {
        return sendNew(title, content, cids);
    }

    /**
     * @param
     * @param template
     * @param msgItem
     * @return
     * @throws
     * @author daitao
     * @version 1.0
     * @description 个推单发
     * @date 2019/4/12
     **/
    private static Map<String, Object> sendSingle(AbstractTemplate template, String cid) {
        IGtPush push = new IGtPush(host, appKey, masterSecret);
        SingleMessage message = new SingleMessage();
        message.setOffline(true);
        // 离线有效时间,单位为毫秒,可选
        message.setOfflineExpireTime(OfflineExpireTime);
        message.setData(template);
        // 可选,1为wifi,0为不限制网络环境。根据手机处于的网络情况,决定是否下发
        message.setPushNetWorkType(0);
        Target target = new Target();
        target.setAppId(appId);
        target.setClientId(cid);
        IPushResult ret;
        try {
            ret = push.pushMessageToSingle(message, target);
        } catch (RequestException e) {
            e.printStackTrace();
            ret = push.pushMessageToSingle(message, target, e.getRequestId());
        }
        return ret.getResponse();
    }

    /**
     * @param
     * @param template
     * @param msgItem
     * @return
     * @throws
     * @author daitao
     * @version 1.0
     * @description 个推单发
     * @date 2019/4/12
     **/
    private static Map<String, Object> sendBatch(AbstractTemplate template, List<String> cids) {
        // 配置返回每个用户返回用户状态,可选
        System.setProperty("gexin_pushList_needDetails", "true");
        IGtPush push = new IGtPush(host, appKey, masterSecret);
        // 通知透传模板
        ListMessage message = new ListMessage();
        message.setData(template);
        // 设置消息离线,并设置离线时间
        message.setOffline(true);
        // 离线有效时间,单位为毫秒,可选
        message.setOfflineExpireTime(OfflineExpireTime);
        // taskId用于在推送时去查找对应的message
        String taskId = push.getContentId(message);
        List<Target> targets = handleTargets(cids);
        IPushResult ret = push.pushMessageToList(taskId, targets);
        return ret.getResponse();
    }

    private static Map<String, Object> sendBatchNew(String title, String content, List<String> cids) {
        Map<String, Object> result = new HashMap<>();

        PushDTO<String> pushDTO = new PushDTO<String>();
        pushDTO.setRequestId(System.currentTimeMillis() + "");
        buildPushMessage(title, content, pushDTO);

        ApiResult<TaskIdDTO> createResult = pushApi.createMsg(pushDTO);
        log.info("sendBatchNew createResult :" + Json.toJson(createResult));
        result.put("code", createResult.getCode());
        if (createResult.isSuccess()) {
            result.put("data", createResult.getData());
            TaskIdDTO taskIdDTO = createResult.getData();
            AudienceDTO pushBatchDTO = new AudienceDTO();
            pushBatchDTO.setTaskid(taskIdDTO.getTaskId());
            Audience audience = new Audience();
            audience.setCid(cids);
            pushBatchDTO.setAudience(audience);
            ApiResult<Map<String, Map<String, String>>> apiResult = pushApi.pushListByCid(pushBatchDTO);
            log.info("sendBatchNew apiResult :" + Json.toJson(apiResult));
            result.put("code", apiResult.getCode());
            if (apiResult.isSuccess()) {
                result.put("data", apiResult.getData());
            } else {
                result.put("msg", apiResult.getMsg());
            }
        } else {
            result.put("msg", createResult.getMsg());
        }
        return result;
    }

    private static List<Target> handleTargets(List<String> cids) {
        List<Target> targets = new ArrayList<>();
        for (String cid : cids) {
            Target target = new Target();
            target.setAppId(appId);
            target.setClientId(cid);
            targets.add(target);
        }
        return targets;
    }

    private static NotificationTemplate buildNotificationTemplate(String title, String content) {
        NotificationTemplate template = new NotificationTemplate();
        // 设置APPID与APPKEY
        template.setAppId(appId);
        template.setAppkey(appKey);
        template.setTransmissionType(transmissionType);
        Style0 style = new Style0();
        // 设置通知栏标题与内容
        style.setTitle(title);
        style.setText(content);
        // 配置通知栏图标
        style.setLogo(logo);
        // 配置通知栏网络图标
        style.setLogoUrl("");
        // 设置通知是否响铃,震动,或者可清除
        style.setRing(isRing);
        style.setVibrate(isVibrate);
        style.setClearable(isClearable);
        template.setStyle(style);
        return template;
    }


    /**
     * @Description
     * @Author daitao
     * @version 1.0
     * @Date 2019/4/24 17:57
     * @Param cids: 1、null或size==0:表示给appId下所有人发
     * 2、size为1:单发
     * 3、size大于1:群发
     * @Return
     * @Exception
     */
    public static Map<String, Object> sendLinkTemplate(String title, String content, String openUrl, List<String> cids) {
        LinkTemplate template = buildLinkTemplate(title, content, openUrl);
        return send(template, cids);
    }


    private static LinkTemplate buildLinkTemplate(String title, String content, String openUrl) {
        LinkTemplate template = new LinkTemplate();
        // 设置APPID与APPKEY
        template.setAppId(appId);
        template.setAppkey(appKey);
        Style0 style = new Style0();
        // 设置通知栏标题与内容
        style.setTitle(title);
        style.setText(content);
        // 配置通知栏图标
        style.setLogo(logo);
        // 配置通知栏网络图标
        style.setLogoUrl("");
        // 设置通知是否响铃,震动,或者可清除
        style.setRing(isRing);
        style.setVibrate(isVibrate);
        style.setClearable(isClearable);
        template.setStyle(style);
        // 设置打开的网址地址
        template.setUrl(openUrl);
        return template;
    }


    /**
     * @Description
     * @Author daitao
     * @version 1.0
     * @Date 2019/4/24 17:57
     * @Param cids: 1、null:表示给appId下所有人发
     * 2、size为1:单发
     * 3、size大于1:群发
     * @Return
     * @Exception
     */
    public static Map<String, Object> sendNotyPopLoadTemplate(String title, String content,
                                                              String popTitle, String popContent, String downloadTitle, String downloadIcon, String downloadUrl, List<String> cids) {
        NotyPopLoadTemplate template = buildNotyPopLoadTemplate(title, content, popTitle, popContent, downloadTitle, downloadIcon, downloadUrl);
        return send(template, cids);
    }


    /**
     * @param title         消息标题
     * @param content       消息内容
     * @param popTitle      弹框标题
     * @param popContent    弹框内容
     * @param downloadTitle 下载标题
     * @param downloadIcon  下载图标
     * @param downloadUrl   下载的url资源地址
     * @return
     */
    private static NotyPopLoadTemplate buildNotyPopLoadTemplate(String title, String content,
                                                                String popTitle, String popContent, String downloadTitle, String downloadIcon, String downloadUrl
    ) {
        NotyPopLoadTemplate template = new NotyPopLoadTemplate();
        // 设置APPID与APPKEY
        template.setAppId(appId);
        template.setAppkey(appKey);
        Style0 style = new Style0();
        // 设置通知栏标题与内容
        style.setTitle(title);
        style.setText(content);
        // 配置通知栏图标
        style.setLogo(logo);
        // 配置通知栏网络图标
        style.setLogoUrl("");
        // 设置通知是否响铃,震动,或者可清除
        style.setRing(isRing);
        style.setVibrate(isVibrate);
        style.setClearable(isClearable);
        template.setStyle(style);
        // 设置弹框标题与内容
        template.setPopTitle(popTitle);
        template.setPopContent(popContent);
        // 设置弹框显示的图片
        template.setPopImage("");
        template.setPopButton1("下载");
        template.setPopButton2("取消");
        // 设置下载标题
        template.setLoadTitle(downloadTitle);
        template.setLoadIcon(downloadIcon);
        //设置下载地址
        template.setLoadUrl(downloadUrl);
        return template;
    }


    /**
     * @Description
     * @Author daitao
     * @version 1.0
     * @Date 2019/4/24 17:57
     * @Param cids: 1、null:表示给appId下所有人发
     * 2、size为1:单发
     * 3、size大于1:群发
     * @Return
     * @Exception
     */
    public static Map<String, Object> sendTransmissionTemplate(String title, String content, List<String> cids) {
        TransmissionTemplate template = buildTransmissionTemplate(title, content);
        return send(template, cids);
    }

    /**
     * 安卓推送透传消息模板
     *
     * @param title
     * @param content
     * @return
     */
    private static TransmissionTemplate buildTransmissionTemplate(String title, String content) {
        TransmissionTemplate template = new TransmissionTemplate();
        template.setAppId(appId);
        template.setAppkey(appKey);
        // 透传消息设置,1为强制启动应用,客户端接收到消息后就会立即启动应用;2为等待应用启动
        template.setTransmissionType(transmissionType);
        template.setTransmissionContent(content);
        return template;
    }


    /**
     * @param oldTaskId 指定需要撤回消息对应的taskId
     * @param force     客户端没有找到对应的taskid,是否把对应appid下所有的通知都撤回
     * @Description
     * @Author daitao
     * @version 1.0
     * @Date 2019/4/24 17:57
     * @Param cids: 1、null:表示给appId下所有人发
     * 2、size为1:单发
     * 3、size大于1:群发
     * @Return
     * @Exception
     */
    public static Map<String, Object> sendRevokeTemplate(String oldTaskId, boolean force, List<String> cids) {
        RevokeTemplate template = getRevokeTemplate(oldTaskId, force);
        return send(template, cids);
    }

    /**
     * 获取撤回模板
     *
     * @param oldTaskId 指定需要撤回消息对应的taskId
     * @param force     客户端没有找到对应的taskid,是否把对应appid下所有的通知都撤回
     * @return
     */
    private static RevokeTemplate getRevokeTemplate(String oldTaskId, boolean force) {
        RevokeTemplate template = new RevokeTemplate();
        template.setAppId(appId);// 应用appid
        template.setAppkey(appKey);// 应用appkey
        template.setOldTaskId(oldTaskId);
        return template;
    }


    /**
     * 发个应用的所有客户端
     *
     * @param template
     * @return
     */
    private static Map<String, Object> sendAllClient(AbstractTemplate template) {
        IGtPush push = new IGtPush(host, appKey, masterSecret);
        // 定义"点击链接打开通知模板",并设置标题、内容、链接
        List<String> appIds = new ArrayList<String>();
        appIds.add(appId);
        // 定义"AppMessage"类型消息对象,设置消息内容模板、发送的目标App列表、是否支持离线发送、以及离线消息有效期(单位毫秒)
        AppMessage message = new AppMessage();
        message.setData(template);
        message.setAppIdList(appIds);
        message.setOffline(true);
        message.setOfflineExpireTime(OfflineExpireTime);
        IPushResult ret = push.pushMessageToApp(message);
        return ret.getResponse();
    }

    private static Map<String, Object> sendAllClientNew(String title, String content) {
        PushDTO<String> pushDTO = new PushDTO<String>();
        pushDTO.setRequestId(System.currentTimeMillis() + "");
        pushDTO.setAudience("all");

        buildPushMessage(title, content, pushDTO);

        ApiResult<TaskIdDTO> apiResult = pushApi.pushAll(pushDTO);

        Map<String, Object> result = new HashMap<>();
        log.info("sendAllClientNew result :" + Json.toJson(apiResult));
        result.put("code", apiResult.getCode());
        if (apiResult.isSuccess()) {
            result.put("data", apiResult.getData());
        } else {
            result.put("msg", apiResult.getMsg());
        }
        return result;
    }
}

pom.xml

<dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-core</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <!--个推-->
        <dependency>
            <groupId>com.gexin.platform</groupId>
            <artifactId>gexin-rp-sdk-http</artifactId>
            <version>4.1.0.1</version>
        </dependency>
        <dependency>
            <groupId>com.gexin.platform</groupId>
            <artifactId>gexin-rp-sdk-base</artifactId>
            <version>4.0.0.26</version>
        </dependency>
        <dependency>
            <groupId>com.gexin.platform</groupId>
            <artifactId>gexin-rp-sdk-template</artifactId>
            <version>4.0.0.20</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/com.gexin.platform/gexin-rp-fastjson -->
        <!-- https://mvnrepository.com/artifact/com.getui.push/restful-sdk -->
        <dependency>
            <groupId>com.getui.push</groupId>
            <artifactId>restful-sdk</artifactId>
            <version>1.0.6.0</version>
        </dependency>

        <dependency>
            <groupId>com.gexin.platform</groupId>
            <artifactId>gexin-rp-fastjson</artifactId>
            <version>1.0.0.1</version>
        </dependency>

仓库

<repository>
            <id>getui</id>
            <url>http://mvn.gt.getui.com/nexus/content/repositories/releases/</url>
        </repository>
202411 月29

Java :请求Https地址,忽略证书

以下以Nutz代码为例:

Header header = Header.create();
        header.addv("Content-Type", "application/json");
        header.addv("AppCode", appcode);
        Request request = Request.create(url, Request.METHOD.POST);
        NutMap body = NutMap.NEW();
        body.put("kssj", kssj);
        body.put("jssj", jssj);
        request.setHeader(header);
        request.setData(Json.toJson(body));
        Sender sender = Sender.create(request).setTimeout(30 * 1000);
        if (url.startsWith("https")) {
            try {
                SSLContext sslcontext = this.createIgnoreVerifySSL();
                sender.setSSLSocketFactory(sslcontext.getSocketFactory());
                sender.setHostnameVerifier((urlHostName, session) -> true);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        Response response = sender.send();
        if (response.isOK()) {
            //todo
        }

private static class TrustAllManager
implements X509TrustManager {
public X509Certificate[] getAcceptedIssuers() {
return null;
}

public void checkServerTrusted(X509Certificate[] certs,
String authType) {
}

public void checkClientTrusted(X509Certificate[] certs,
String authType) {
}
}


public SSLContext createIgnoreVerifySSL() throws NoSuchAlgorithmException, KeyManagementException {
SSLContext sc = SSLContext.getInstance("SSL");
sc.init(null, new TrustManager[]{new CityLifelineServer.TrustAllManager()}, null);
return sc;
}

202410 月24

MySQL 8:授权用户远程访问语句

ALTER USER 'root'@'%' IDENTIFIED BY 'pwd' PASSWORD EXPIRE NEVER; 

ALTER USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY 'pwd'; 

FLUSH PRIVILEGES;
20249 月13

PVE:N305 飞牛OS 显卡直通

pve 8.2

1、第一步,升级pve内核到最新

1)设置dns,让源域名可以解析

2)设置国内源

#编辑 Debian sources.list 源文件,写入国内源
nano /etc/apt/sources.list

# 内容如下:
deb https://mirrors.ustc.edu.cn/debian/ bookworm main contrib
deb-src https://mirrors.ustc.edu.cn/debian/ bookworm main contribe
deb https://mirrors.ustc.edu.cn/debian/ bookworm-updates main contrib
deb-src https://mirrors.ustc.edu.cn/debian/ bookworm-updates main contrib

# 编辑 PVE 源文件 pve-no-subscription.list
nano /etc/apt/sources.list.d/pve-no-subscription.list

# 内容如下:
deb https://mirrors.tuna.tsinghua.edu.cn/proxmox/debian bookworm pve-no-subscription

# 编辑屏蔽 PVE 企业源文件
nano /etc/apt/sources.list.d/pve-enterprise.list

# 将下面这一行注释掉 (前面加上井号)
# deb https://mirrors.tuna.tsinghua.edu.cn/proxmox/debian bookworm pve-no-subscription

3)升级内核

数据中心 – pve 节点 – 更新 – 升级

升级结束重启

2、第二步,虚拟机先安装系统

正常的虚拟机配置(默认配置),安装好飞牛OS系统。

BIOS选择 OVMF(UEFI)

BIOS 关闭 Secure Boot

处理器类型选择 host

显卡 默认

机型 默认

3、第三步,设置显卡直通

1)虚拟机停机,如果pve控制台停机不了,则网页进入飞牛系统关机。

pve节点配置参数

nano /etc/pve/qemu-server/101.conf
args%3A -set device.hostpci0.addr=02.0

飞牛os虚拟机设置:

2)显卡 选择 无

3)机型 选择 q35

5)添加PCI设备,选择原始设备 0000:00:02.0,勾选所有功能,主GPU、PCI-Express 不要勾选。

6)启动虚拟机,飞牛OS设置中查看是否显示GPU,影视播放启用硬解,并播放其他分辨率查看GPU占用情况。

ps:不同主机的GPU编号可能不同,比如 0000:00:02.0 ,device.hostpci0.addr=02.0

20249 月9

PVE 下虚拟机重启时间晚八小时

虚拟机 – 选项 – 使用本地进行RTC:默认 改为 是

20249 月8

解决alist3 挂载本地存储(U盘) 无权访问问题

http://10.10.10.3:5244/dav/usb

`sudo chown -R alist3 SSK`

20248 月27

Docker 跑 tinyMediaManager

第一步:创建容器

docker create --name=tinymediamanager \
-e GROUP_ID=0 -e USER_ID=0 -e TZ=Asia/Shanghai \
-p 5800:5800 \
-p 5900:5900 \
dzhuang/tinymediamanager:latest-v5

PS: 网络已通过 https://wizzer.cn/archives/3804 配好,可以自动 pull 镜像文件

第二步:磁盘映射

NAS路径 -> 映射路径

/docker/ttm/config -> /config
/video -> /video

http://10.10.10.10:5800 访问,即可查看界面。

20248 月26

中国历史一图流

转自:

20247 月23

BudIot 开源物联网设备平台v1.0发布

BUDIOT 是一个开源的、企业级的物联网平台,它集成了设备管理、协议解析、消息订阅、场景联动等一系列物联网核心能力,支持以平台适配设备的方式连接海量设备,支持在线下发指令实现远程控制,支持扩展水电气等各类计费业务场景。

本平台是在千万级设备实时计费物联网平台经验基础上,在不损失性能的前提下进行功能删减、结构优化而来,小而美,同时又具备灵活的扩展性。

源码: https://github.com/budwk/budiot

在线演示地址: https://demo.budiot.com 用户名: superadmin 密码: 1

官网: https://budiot.com

开发框架

基于自研 Java 微服务框架 https://budwk.com

简单说明

Jar 运行模块

  • budiot-access/budiot-access-gateway 设备网关,用于设备协议和 network 组件
  • budiot-access/budiot-access-processor 设备数据上报业务处理模块
  • budiot-server WEB 服务 API ,定时任务等

其他模块说明

  • budiot-access/budiot-access-network 网络组件,支持 TCP/MQTT/UDP/HTTP 等
  • budiot-access/budiot-access-protocol 设备协议开发包,内含 demo 示例
  • budiot-access/budiot-access-storage 设备数据存储,可扩展时序数据库等

前端模块

  • budiot-vue-admin Vue3 + Element-Plus

开发环境

  • OpenJDK 11
  • Redis 6.x
  • MariaDB 10.x
  • MongoDB 7.0.x
  • RocketMQ 5.2.x

设备上报有效数据存储

默认采用 MongoDB 7 的时序集合,可根据项目规模需要,扩展为 TDEngine 等时序数据库

20247 月16

为群晖 Container Manager 配置代理

原文地址:https://blog.chai.ac.cn/posts/docker-proxy

最后更新:2024年7月15日

最近又见识到了一些神奇的骚操作,考虑到在将来 Docker 的国内各个镜像站可能变得不可用,需要未雨绸缪一下。 有旁路由自然是好的,但现在打算用 Proxy 来解决这个问题。 由于群晖的 Container Manager 是基于 Docker 的,但部分配置路径不同,所以特意记录一下。

注意事项:

  • 本教程在全新的 Ubuntu 22.04 LTS 系统环境下通过测试
  • 本教程在群晖 DSM 7.2 版本通过测试,假定具备 root 权限
  • 假定你已有可以代理 HTTP 或 SOCKS 协议的端口:例如 192.168.50.100:7893
  • 尽可能引用官方文档,本文主要针对需要代理的部分的设置

安装 Docker,为 apt 设置代理

群晖 Container Manager 用户可以跳过这一小节,你实际上已经有 Docker 了。

Docker Engine 安装过程请参考 官方文档

Docker 官方给出了许多安装方式,我选择用 apt 从官方维护的源中安装。

你也可以选择手动下载二进制包,然后用 dpkg 安装.

这里选择使用 apt 演示,关键在于很多人还不清楚如何为 apt 设置代理:

shell

sudo vi /etc/apt/apt.conf

shell

Acquire::http::Proxy "http://192.168.50.100:7893";
Acquire::https::Proxy "http://192.168.50.100:7893";

注意第二个行依旧是 http 协议,否则会碰到 TLS Could not handshake 问题。 代理服务器只需要负责做请求转发和响应转发,不会像 HTTPS 协议一样进行解密和加密。

为 Docker Daemon 设置代理

安装完成后,官网教程会让你运行 docker run hello-world 来验证安装是否成功。

默认情况下,你的本地肯定不存在任何有关镜像(如下所示),因此会从官方库拉取:

shell

Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
c1ec31eb5944: Retrying in 1 second
docker: error pulling image configuration: 
  download failed after attempts=6: dial tcp 31.13.82.33:443: i/o timeout.

执行拉取操作的是 Docker Daemon,参考 官方文档 进行 Proxy 有关的设置:

对于 Docker 引擎 23.0 及更高版本

提示

使用 docker info 可以查询到版本信息,版本太低的话请参考下一节的方法。

Docker Daemon 大多数配置选项都可根据 daemon.json 文件进行设置。

对于 Docker 引擎 23.0 及更高版本,可以在该文件中设置代理行为:

  • Root 模式:/etc/docker/daemon.json
  • Rootless 模式:~/.config/docker/daemon.json
  • 群晖 Container Manager:/var/packages/ContainerManager/etc/docker.json

shell

{
  "proxies": {
    "http-proxy": "http://192.168.50.100:7893",
    "https-proxy": "http://192.168.50.100:7893",
    "no-proxy": "127.0.0.0/8"
  }
}

这些配置将覆盖 docker.service 默认的 systemd 设定。

如果您位于 HTTP 或 HTTPS 代理服务器后面,例如在公司设置中, 则必须在 systemd 服务文件中指定守护程序代理配置,而不是在 daemon.json 文件中或使用环境变量。

较为通用的 systemd 设置方法

如 Docker 版本太低,不支持通过 daemon.json 配置代理,则需手动创建 systemd 文件:

  • Root 模式:/etc/systemd/system/docker.service.d
  • Rootless 模式:~/.config/systemd/user/docker.service.d
  • 群晖:/etc/systemd/system/pkg-ContainerManager-dockerd.service.d

添加 http-proxy.conf 文件,下面以群晖 Container Manager 为例:

shell

sudo mkdir -p /etc/systemd/system/pkg-ContainerManager-dockerd.service.d
sudo vi /etc/systemd/system/pkg-ContainerManager-dockerd.service.d/http-proxy.conf

shell

[Service]
Environment="HTTP_PROXY=http://192.168.50.100:7893"
Environment="HTTPS_PROXY=http://192.168.50.100:7893"
Environment="NO_PROXY=localhost,127.0.0.1"

如果你有内建的 registry-mirrors, 记得加入 NO_PROXY 中。

重启 Docker Daemon

不论采用上面哪种方式,都需要重启 Docker Daemon 服务:

  • 如果是旧版本群晖(6.0+),要用 synoservice 代替 systemctl.
  • 如果是 rootless 模式,要用 systemctl --user 代替 sudo systemctl.

下面仅仅给出 root 模式和群晖 Container Manager 的重启方法:

sudo systemctl daemon-reload
sudo systemctl restart docker
sudo systemctl restart pkg-ContainerManager-dockerd.service

重启 Docker/Conatiner Manager 服务需要一定的时间,取决于你正在运行的容器数量。

检查设置是否生效:

sudo systemctl show --property=Environment docker
systemctl show --property=Environment pkg-ContainerManager-dockerd.service

再次跑 docker run hello-world,应该就能成功了。

为 Docker 容器设置代理

有的时候,你使用的 Docker 镜像在 build 和 run 时也需要代理。 大部分应该都知道怎么配置,或者会通过环境变量来设置。 但有的时候希望代理配置默认对所有容器生效(那为什么不用机器或路由级别的代理呢),可以参考下面的方法。

参考 官方文档 中的说明,你可以在 ~/.docker/config.json 中设置代理。

shell

{
 "proxies": {
   "default": {
     "httpProxy": "http://192.168.50.100:7893",
     "httpsProxy": "http://192.168.50.100:7893",
     "noProxy": "127.0.0.0/8"
   }
 }
}

保存文件后配置将生效,适用于新容器的生成和运行,无需重启 Docker,

本质上,它通过影响 Docker CLI 来添加环境变量,效果类似于:

shell

docker build --build-arg HTTP_PROXY="http://192.168.50.100:7893" .
docker run --env HTTP_PROXY="http://192.168.50.100:7893" redis

但一般还是建议单独针对需要代理服务的容器手动设置这些环境变量, 同样地,一些 Docker 内的应用是不按照环境变量来设置代理的,需要手动配置,需要额外注意。 折腾了这么多,是不是还是觉得旁路由+规则代理的方法会更加简单呢?这就看个人需求了。

20247 月9

Java项目运行一段时间后报错 Comparison method violates its general contract

java.util.concurrent.ExecutionException: java.lang.IllegalArgumentException: Comparison method violates its general contract!

错误代码:
children.sort((o1, o2) -> (int) (o2.getLong("createdAt", 0) - o1.getLong("createdAt", 0)));

修复后:
children.sort((o1, o2) -> Long.compare(o2.getLong("createdAt", 0), o1.getLong("createdAt", 0)));

排序器 children.sort((o1, o2) -> (int) (o2.getLong("createdAt", 0) - o1.getLong("createdAt", 0))); 存在一个潜在的问题,即当时间戳的差值超出 int 的范围时,强制转换为 int 会导致数据溢出,从而导致比较结果不正确。

一个解决方法是使用 Long.compare 方法进行比较,而不是手动进行减法运算。Long.compare 方法确保了比较的对称性和一致性。

20247 月8

600万设备连接平台遇到的坑

MongoDB 的坑

listCollections

  • 问题:代码中存在 listCollections 操作,大量数据上报时,造成MongoDB CPU高升,使处理性能受到影响。经过检查,发现通信报文日表是自动创建的,每次都会判断集合是否存在;而mongo驱动包里判断集合是否存在的操作,就是先执行 listCollections;
  • 优化:提前创建好需要的集合,不要在数据上报的时候进行判断、创建;ps:其实其他数据库,比如mysql、tdengine等,都是同样的道理;

连接数过大

  • 问题:MongoDB 连接数配置较大,导致很多handler服务,过多的线程会导致上下文切换开销变大,同时内存开销也会上涨;
  • 优化:调低连接数配置,进行压力测试;

规则引擎的坑

  • 问题:为了提升性能,往往采用队列+规则引擎来处理业务,设备协议解析+计费业务是在一个handler里,而规则引擎则在下一个队列处理;规则引擎负责为欠费的表具创建短信提醒、关阀指令(产品欠费规则配置),往往在第一个队列里需等待N秒,将规则引擎创建的指令一起下方给表具,每天几百万表具实时上报数据+实时计费,数据并发量较大,如果都等待N秒,会严重影响处理性能;
  • 优化:对计费后余额大于等于0的的表具,不等待N秒,直接回复结束指令,绝大部分表具其实已经是关阀状态,无需本次下发关阀指令,哪怕有需要关阀的,延后到下次通信再执行也没影响;对于小于0的表具,则使用原有逻辑;优化后,降低了90%的等待时间,大大提升高峰高并发处理性能;

HTTP订阅的坑

  • 问题:前期主要通过AEP平台的http订阅实现NB表的通信,但是http服务在高并发时有时候会挂掉(具体表象就是两个http服务,只有一个存活,另一个服务存在,但不处理数据);
  • 优化:将http服务单独服务部署,不要和其他handler、服务抢占服务器资源,jvm也就不会崩了;后期采用MQ订阅方式;

站内信的坑

  • 问题:原有产品设计当设备产生告警时(原生告警、规则告警等)有站内信提醒,经过项目实际运行,站内信数据非常庞大,时刻都有设备告警,导致web页面卡顿、后台web服务资源占用高;
  • 优化:去掉设备站内信告警功能,一是站内信管理人员完全看不过来,二是设备告警在功能菜单里可以查询到;

20247 月5

MongoDB 为什么要限制连接数

Mongod 的服务模型是每个网络连接由一个单独的线程来处理,每个线程配置了1MB 的栈空间,当网络连接数太多时,过多的线程会导致上下文切换开销变大,同时内存开销也会上涨。

  • 连接是要消耗资源的,而且消耗的并不少。
    • 内存:MongoDB为例,每个线程都要分配1MB的栈内存出来。1000个连接,1G内存就这么没了,甭管是否是活跃连接
    • 文件句柄:每个连接都要打开一个文件句柄,当然从成本上讲,这个消耗相对内存是小了很多。但换个角度,文件句柄也被其他模块消耗着,比如WT存储引擎,就需要消耗大量的文件句柄
  • 是否真的需要这么多的链接,一般的业务场景下请求压力在1000QPS左右,按照每个请求50ms计算,最多也就需要1000/(1000/50)==50个链接即可满足需求,并且是整个系统50个链接即可。
  • 很多人平时没有怎么注意过链接数概念,上云后发现居然有这样的限制,心里很不舒服,可能非常不理解。这里说下常见的两种情况:
    • 短链接:一般都是PHP环境,因为PHP的框架决定了PHP短链接的特性,并且链接数的需求一般是在1000-3000左右,具体多少还要根据业务部署的PHP数量来计算。并且MongoDB开源版本在短链接Auth处理上并不优雅,会消耗非常多的CPU资源,3000链接即可跑满24Core的CPU。PHP大拿Facebook也有同样的问题,所以他们用go语言自行开发了一套Proxy代理,来解决对MongoDB的短链接请求问题,但这毕竟带来部署成本和兼容性问题。
    • 长链接:比较健康合理的使用方式,但是也要正确的配置客户端,相关的参数为&maxPoolSize=xx 在ConnectionURI上追加上去即可,否则默认每个客户端就是高处100来个,平白的浪费资源
  • 链接数的上限需要综合考虑性能,稳定性,业务需求。多方面去考虑,缺一不可。超低的内存,配置超高的链接数,得到的只能是OOM。
20246 月20

Odoo 17.0 源码部署的坑

启动需指定配置文件(配置数据库用户和密码,用户不可为默认用户)

python3 odoo-bin -c debian/odoo.conf -i base -d odoo

配置文件

[options]
; This is the password that allows database operations:
; admin_passwd = admin
db_host = 127.0.0.1
db_port = 5432
db_user = odoo
db_password = 1234567890
addons_path = /data/odoo/addons
default_productivity_apps = True

需安装依赖

pip3 install -r requirements.txt

20246 月20

PostgreSQL 常用命令

  • 切换用户

su postgres

pgsql

  • 常用命令

\l 列出数据库

\du 列出用户

\q 退出窗口

  • 修改密码

su – postgres

pgsql

select usename,passwd from pg_shadow;

ALTER USER demo with password ‘demo12345678’;

或创建用户

CREATE USER demo WITH PASSWORD ‘demo12345678’;

赋予权限

ALTER USER demo WITH SUPERUSER;

ALTER USER demo WITH CreateDB;

创建数据库

CREATE DATABASE demo OWNER demo;
20246 月19

企业微信:数据与智能专区 docker打包的坑

1、文档说明要求 python 3版本,但没有说必须是 3.6 版本,其他版本报错找不到 libpython3.6m.so.1.0;

2、使用 python 3.6 得安装 requests 组件,文档没有说明;

3、部署时必须指定执行的 sh 脚本文件;

Dcokerfile 文件内容如下:

FROM python:3.6-slim

WROKDIR /app

COPY . /app

RUN python -m pip install --upgrade pip

RUN pip install requests

RUN pip install pycryptodome

EXPOSE 8080

ps:搞过微信开发的都知道,微信的开发文档到底是多么多么的……

20246 月12

backup:招聘模版

前端开发工程师

职位描述:
1、负责与开发组长沟通业务需求,与后端开发人员对接后端API、开发前端页面;
2、负责修改公司现有软件项目BUG,并对前端功能的持续优化改进;
3、负责公司现有前端框架和组件的维护、改进;

职位要求:
1、2年以上前端开发经验;
2、熟悉Web前端开发基础技能(HTML/CSS/JavaScript);
3、熟悉Vue2、Vue3、ElementUI、Element-Plus等前端开发框架;
4、对前端工程化与模块化开发有一定了解,有webpack/vite/npm实践经验;
5、有GIS开发经验(电子围栏、轨迹、图层等)、数据大屏开发经验者优先;
6、对前端开发有浓厚兴趣,能主动学习;
7、有良好的编码习惯,重视代码质量;
8、善于沟通,工作积极,有责任心,善于协作与分享;

测试运维工程师

岗位职责:
1、负责Java软件项目的全流程测试,包括制定测试计划、编写测试方案和测试用例等;
2、负责Java软件项目的功能、性能等方面的测试工作,执行测试用例,提交BUG,并进行BUG跟踪和回归测试,直到BUG解决;
3、负责Java软件项目的日常更新维护,包括Docker容器化部署等环境;
4、负责收集、跟进客户的使用问题,直到问题被有效解决;

任职资格:
1、本科及以上学历,计算机或相关专业,3年以上软件测试经验;
2、熟悉软件研发、测试流程,了解过程标准和规范,能主动在关键过程节点推动任务执行;
3、熟悉软件测试方法和软件工程知识,流程意识强,具备有效发现问题和解决问题的能力;
4、熟悉Docker部署,熟练掌握Linux服务器的配置和管理;
5、了解常见开源软件的集群化部署、维护和使用,例如RabbitMQ,MongoDB,Redis,MySQL/MariaDB、ElasticSearch等
6、具有一定的文档编写能力,如部署文档、测试方案、测试用例等;

20241 月25

el-table 尾部汇总放在第一行

调用页面添加样式

<style scoped>
::v-deep .el-table {
    display: flex;
    flex-direction: column;
}
::v-deep .el-table__body-wrapper {
    order: 1;
}
::v-deep .el-table__fixed-body-wrapper {
    top: 92px !important;
}
::v-deep .el-table__fixed-footer-wrapper {
    z-index: 0;
    top: 45px;
}
</style>

分页组件完整代码

<template>
    <div class="data-list">
        <transition name="el-fade-in">
            <div v-show="selectRows.length > 0" class="el-selection-bar">
                选择了<span class="el-selection-bar__text">{{ selectRows.length }}</span
                >条数据
                <a class="el-selection-bar__clear" @click="clearSelection">清空</a>
            </div>
        </transition>
        <div v-loading="isRequestIng" class="data-list__table" :element-loading-text="loadingTxt">
            <span v-if="summary">总条数:{{ rows.length }}</span>
            <el-table
                v-bind="$attrs"
                ref="tabList"
                :data="rows"
                v-on="$listeners"
                :border="true"
                :span-method="handleRowSpanMethod"
                :show-summary="summary"
                sum-text=" 汇  总 "
            >
                <slot />
                <pro-empty slot="empty" />
            </el-table>
        </div>
        <div v-if="!summary && (pageData.totalCount > pageData.pageSize || pageData.pageSize > 10)" class="data-list__pager">
            <el-pagination
                :current-page="pageData.pageNo"
                :page-size="pageData.pageSize"
                :total="pageData.totalCount"
                background
                :page-sizes="pageSizes"
                layout="total, ->, prev, pager, next, sizes, jumper"
                @current-change="doChangePage"
                @size-change="doSizeChange"
            />
        </div>

    </div>
</template>

<script>
import { forIn, findIndex, cloneDeep, remove, uniqBy, concat, isArray, first } from "lodash-es"
import { f } from 'vue-marquee-component'
export default {
    name: "PlusTableList",
    props: {
        server: {
            type: String,
            require: true,
            default: ""
        },
        methods: {
            type: String,
            default: "post"
        },
        lazy: {
            type: Boolean,
            default: false
        },
        data: {
            type: Object,
            default: () => {}
        },
        dataFilter: {
            type: Function,
            default: data => data
        },
        loadingTxt: {
            type: String,
            default: "数据加载中..."
        },
        paramClear: {
            type: Boolean,
            default: false
        },
        selectRows: {
            type: Array,
            default: () => []
        },
        selectable: {
            type: Function,
            default: () => true
        },
        rowKey: {
            type: String,
            default: "id"
        },
        selection: {
            type: Boolean,
            default: true
        },
        pageSizes: {
            type: Array,
            default: () => [10, 20, 30, 50]
        },
        // 合并行(第一列)
        spanName0: {
            type: String,
            default: null
        },
        // 合并行(第二列)
        spanName1: {
            type: String,
            default: null
        },
        // 是否汇总数据
        summary: {
            type: Boolean,
            default: false
        }
    },
    data() {
        return {
            pageData: {
                pageNo: 1,
                pageSize: 10,
                totalCount: 0
            },
            rows: [],
            isRequestIng: false
        }
    },
    watch: {
        pageSizes: {
            handler: function (val) {
                if (isArray(val)) {
                    this.pageData.pageSize = first(val)
                }
            },
            immediate: true
        }
    },
    mounted() {
        if (!this.lazy) {
            this.getList()
        }
    },
    methods: {
        // 合并相同值的行
        handleRowSpanMethod({ row, column, rowIndex, columnIndex }) {
            if (columnIndex === 0) {
                if (!this.spanName0) return
                if (rowIndex > 0 && row[this.spanName0] === this.rows[rowIndex - 1][this.spanName0]) {
                    return {
                        rowspan: 0,
                        colspan: 0
                    }
                } else {
                    let count = 1
                    for (let i = rowIndex + 1; i < this.rows.length; i++) {
                        if (row[this.spanName0] === this.rows[i][this.spanName0]) {
                            count++
                        } else {
                            break
                        }
                    }
                    if(count>1){
                        return {
                            rowspan: count,
                            colspan: 1
                        }
                    }
                }
            }
            if (columnIndex === 1) {
                if (!this.spanName1) return
                // 第一列值相同,且第二列值相同的的情况下合并
                if (rowIndex > 0 && row[this.spanName0] === this.rows[rowIndex-1][this.spanName0] && row[this.spanName1] === this.rows[rowIndex - 1][this.spanName1]) {
                    return {
                        rowspan: 0,
                        colspan: 0
                    }
                } else {
                    let count = 1
                    for (let i = rowIndex + 1; i < this.rows.length; i++) {
                        // 第一列值相同,且第二列值相同的的情况下合并
                        if (row[this.spanName0] === this.rows[i][this.spanName0] && row[this.spanName1] === this.rows[i][this.spanName1]) {
                            count++
                        } else {
                            break
                        }
                    }
                    if(count>1){
                        return {
                            rowspan: count,
                            colspan: 1
                        }
                    }
                }
            }
        },
        // 页码变动事件
        doChangePage(val) {
            this.pageData.pageNo = val
            this.getList()
        },
        // 页大小变动事件
        doSizeChange(val) {
            this.pageData.pageSize = val
            this.pageData.pageNo = 1
            this.getList()
        },
        getList() {
            const { totalCount, ...pager } = this.pageData
            const params = Object.assign({}, this.data, pager)
            if (this.paramClear) {
                forIn(params, (value, key) => {
                    if (value === "") delete params[key]
                })
            }
            this.isRequestIng = true
            this.$get(params)
                .then(({ data }) => {
                    this.rows = this.dataFilter(data.list || [])
                    this.pageData.totalCount = data.totalCount
                    this.$emit("updateTotal", data.totalCount)
                    this.isRequestIng = false
                    this.$nextTick(() => {
                        this.handlePageUpdate()
                    })
                })
                .catch(error => {
                    this.isRequestIng = false
                })
        },
        handlePageUpdate() {
            const list = this.rows
            list.forEach(row => {
                if (
                    findIndex(this.selectRows, el => {
                        return el[this.rowKey] === row[this.rowKey]
                    }) !== -1
                ) {
                    this.$refs.tabList.toggleRowSelection(row, true)
                }
            })
        },
        handleSelectionChange(val) {
            const selectRows = cloneDeep(this.selectRows)
            this.$nextTick(() => {
                const list = this.rows.map(el => el[this.rowKey])
                remove(selectRows, el => {
                    return list.includes(el[this.rowKey])
                })
                this.$emit(
                    "update:selectRows",
                    uniqBy(concat(selectRows, val), el => el[this.rowKey])
                )
            })
        },
        $get(data) {
            if (this.methods === "get") {
                return this.$axios.get(this.server, {
                    params: data
                })
            } else {
                return this.$axios.post(this.server, data)
            }
        },
        reset() {
            this.pageData.pageNo = 1
            this.pageData.totalCount = 0
            this.rows = []
            this.clearSelection()
        },
        async clearSelection() {
            this.$refs.tabList.clearSelection()
            await this.$nextTick()
            this.$emit("update:selectRows", [])
        },
        query() {
            this.reset()
            this.getList()
        }
    }
}
</script>