Graphviz

画图工具

Kibana

Kibana 是 Elasticsearch 的开源数据可视化仪表板。

nexus

maven 仓库

PhantomJS

有时,我们需要浏览器处理网页,但并不需要浏览,比如生成网页的截图、抓取网页数据等操作。PhantomJS 的功能,就是提供一个浏览器环境的命令行接口,你可以把它看作一个“虚拟浏览器”,除了不能浏览,其他与正常浏览器一样。它的内核是 WebKit 引擎,不提供图形界面,只能在命令行下使用,我们可以用它完成一些特殊的用途。

php tool

  • wamp
  • phpstudy

wareshark

抓包工具

NSSM

NSSM is a service helper program similar to srvany and cygrunsrv. It can
start any application as an NT service and will restart the service if it
fails for any reason.

天若 OCR 文字识别

pxcook

前端测量工具

apache-jmeter

性能测试工具

常见问题

大Key/热Key分析/整库扫描_分布式缓存服务 DCS_常见问题_华为云

备份策略

RDB

rdb:指定时间点上生成的数据集快照

优点:

  1. 保存某个时间点上的数据;
  2. 文件紧凑,恢复快;方便传输
  3. 父进程不受 IO 影响,是从父进程中 fork 一份出来备份的;

缺点:

  1. 可能造成备份期间的数据丢失;
  2. fork 过程可能比较耗时(数据集大时,消耗的 CPU 可能导致无法响应客户端)

AOF

aof:所有写操作命令(redis 协议),恢复时执行这些命令
优点:

  1. 灵活配置 fsync 策略;1)无 fsync;2)每秒一次;3)每次写入都 fsync;(就算发生了故障也是 1 秒,性能也不错;fsync 在后台线程执行)
  2. 对 aof 日志文件只进行追加操作;
  3. 文件变大时,会进行重写;
  4. 以 redis 协议写入日志,易读和修复(flushall)

缺点:

  1. 相对 rdb,体积更大;
  2. fsync 策略可能会比 rdb 慢
  3. 个别 bug

总结:
如果想要非常高的安全性,可以结合 rdb 和 aof 同时使用;在重启的时候,会优先使用 aof 来恢复数据(aof 更完整);

其他:
rdb: 默认存入到 dumb.rdb 文件中;
RDB 手动备份:
SAVE:阻塞主进程,此时客户端无法连接;
BGSAVE:fork 一个子线程,不阻塞主进程,客户端依然可以连接;完成后通知主进程,子进程退出;
可以设置多少秒内有多少变动时触发备份

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#save <seconds> <changes>
#
# Will save the DB if both the given number of seconds and the given
# number of write operations against the DB occurred.
#
# In the example below the behaviour will be to save:
# after 900 sec (15 min) if at least 1 key changed
# after 300 sec (5 min) if at least 10 keys changed
# after 60 sec if at least 10000 keys changed
#
# Note: you can disable saving completely by commenting out all "save" lines.
#
# It is also possible to remove all the previously configured save
# points by adding a save directive with a single empty string argument
# like in the following example:
#
# save ""
# 时间窗口M和改动的键个数N,每当时间M内改动的键个数大于N时,则触发快照备份。
save 900 1
save 300 10
save 60 10000

AOF 重写:
bgrewrite 可以用来重写,重新生成一个文件,此文件只包含当前数据集需要的最少的指令(因为是 append 的方式 ,对同一个 key 进行操作也会追加多次指令)

参考资料

1
2
3
4
5
6
7
8
9
10
ext {
rxjavaVersion = '2.1.1'
rxandroidVersion = '2.0.1'
retrofitVersion = '2.3.0'
}
implementation "io.reactivex.rxjava2:rxandroid:$rxandroidVersion"
implementation "io.reactivex.rxjava2:rxjava:$rxjavaVersion"
implementation "com.squareup.retrofit2:retrofit:$retrofitVersion"
implementation "com.squareup.retrofit2:adapter-rxjava2:$retrofitVersion"
implementation "com.squareup.retrofit2:converter-gson:$retrofitVersion"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class NetWork {

private static Map<String, Object> map = new HashMap<>();

public static <T> T get(@NonNull String baseUrl, @NonNull Class<T> clazz) {
String key = clazz.getSimpleName().concat(baseUrl);
if (map.containsKey(key)) {
//noinspection unchecked
return (T) map.get(key);
}
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(baseUrl)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.build();
T t = retrofit.create(clazz);
map.put(key, t);
return t;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class NetWorkTest {

@Test
public void testIp() {
List<String> ips = Arrays.asList(
"104.194.84.57"
, "14.194.84.55"
, "13.194.84.55"
);
CountDownLatch latch = new CountDownLatch(ips.size());
CompositeDisposable compositeDisposable = new CompositeDisposable();

compositeDisposable.addAll(ips.stream()
.map(s -> getDisposable(latch, s))
.toArray(Disposable[]::new));
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}

private Disposable getDisposable(CountDownLatch latch, String ip) {
return NetWork.get("http://ip-api.com/", IpService.class)
.getIpDetail(ip)
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.computation())
.subscribe(ipDetail -> {
latch.countDown();
System.out.println("city:" + ipDetail.getCity());
}, throwable -> {
latch.countDown();
System.out.println("error:" + throwable);
});
}
}
1
2
3
4
public interface IpService {
@GET("json/{ip}")
Observable<IpDetail> getIpDetail(@Path("ip") String ip);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
public class IpDetail {

private String as;
private String city;
private String country;
private String countryCode;
private String isp;
private double lat;
private double lon;
private String org;
private String query;
private String region;
private String regionName;
private String status;
private String timezone;
private String zip;

public String getAs() {
return as;
}

public void setAs(String as) {
this.as = as;
}

public String getCity() {
return city;
}

public void setCity(String city) {
this.city = city;
}

public String getCountry() {
return country;
}

public void setCountry(String country) {
this.country = country;
}

public String getCountryCode() {
return countryCode;
}

public void setCountryCode(String countryCode) {
this.countryCode = countryCode;
}

public String getIsp() {
return isp;
}

public void setIsp(String isp) {
this.isp = isp;
}

public double getLat() {
return lat;
}

public void setLat(double lat) {
this.lat = lat;
}

public double getLon() {
return lon;
}

public void setLon(double lon) {
this.lon = lon;
}

public String getOrg() {
return org;
}

public void setOrg(String org) {
this.org = org;
}

public String getQuery() {
return query;
}

public void setQuery(String query) {
this.query = query;
}

public String getRegion() {
return region;
}

public void setRegion(String region) {
this.region = region;
}

public String getRegionName() {
return regionName;
}

public void setRegionName(String regionName) {
this.regionName = regionName;
}

public String getStatus() {
return status;
}

public void setStatus(String status) {
this.status = status;
}

public String getTimezone() {
return timezone;
}

public void setTimezone(String timezone) {
this.timezone = timezone;
}

public String getZip() {
return zip;
}

public void setZip(String zip) {
this.zip = zip;
}
}

model

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import com.alibaba.fastjson.annotation.JSONField;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonProperty;

import java.util.Date;

public class Model {
@JSONField(name = "fastjson_name") // fastjson 改名
@JsonProperty("jackson_name") // jackson 改名
private String name = "name";

@JSONField(name = "fastjson_time", format = "yyyy-MM-dd hh:mm") // fastson 修改日期格式
@JsonProperty("jackson_time")
@JsonFormat(pattern = "yyyy/MM/dd hh:mm:ss") // jackson 修改日期格式
private Date time;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public Date getTime() {
return time;
}

public void setTime(Date time) {
this.time = time;
}

}

controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@RestController
public class MyController {

@RequestMapping("/jackson")
public Model jackson() {
Model model = new Model();
model.setTime(new Date());
return model;
}

@RequestMapping("/fastjson")
public Object fastjson() {
Model model = new Model();
model.setTime(new Date());
// 这里也可以再次指定dateFormat,会覆盖之前的
return JSON.toJSONStringWithDateFormat(model, null, SerializerFeature.DisableCircularReferenceDetect);
}
}

结果

1
2
3
4
5
// http://localhost:8080/jackson
{
"jackson_name": "name",
"jackson_time": "2020/11/02 02:52:03"
}
1
2
3
4
5
// http://localhost:8080/fastjson
{
"fastjson_name": "name",
"fastjson_time": "2020-11-02 10:51"
}

JAVA 正则表达式:Pattern 类与 Matcher 类详解(转) - ggjucheng - 博客园

1
2
3
4
5
6
7
Pattern p=Pattern.compile("\\d+");
Matcher m=p.matcher("我的QQ是:456456 我的电话是:0532214 我的邮箱是:aaa123@aaa.com");
while(m.find()) {
System.out.println(m.group());
System.out.print("start:"+m.start());
System.out.println(" end:"+m.end());
}

现在大家应该知道,每次执行匹配操作后 start(),end(),group()三个方法的值都会改变,改变成匹配到的子字符串的信息,以及它们的重载方法,也会改变成相应的信息.
注意:只有当匹配操作成功,才可以使用 start(),end(),group()三个方法,否则会抛出 java.lang.IllegalStateException,也就是当 matches(),lookingAt(),find()其中任意一个方法返回 true 时,才可以使用.

Pattern 类:
pattern 对象是一个正则表达式的编译表示。Pattern 类没有公共构造方法。要创建一个 Pattern 对象,你必须首先调用其公共静态编译方法,它返回一个 Pattern 对象。该方法接受一个正则表达式作为它的第一个参数。

Matcher 类:
Matcher 对象是对输入字符串进行解释和匹配操作的引擎。与 Pattern 类一样,Matcher 也没有公共构造方法。你需要调用 Pattern 对象的 matcher 方法来获得一个 Matcher 对象。

PatternSyntaxException:
PatternSyntaxException 是一个非强制异常类,它表示一个正则表达式模式中的语法错误。

BigDecimal 相等问题

切记,不能根据字面来判断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class BigDecimalTest {
public static void main(String[] args) throws Exception {
String num1 = "0";
String num2 = "0.00";
BigDecimal bd1 = new BigDecimal(num1);
BigDecimal bd2 = new BigDecimal(num2);
boolean result1 = bd1.equals(bd2);
boolean result2 = bd1.compareTo(bd2) == 0;
System.out.println("result1:" + result1);
System.out.println("result2:" + result2);
}
}
/*
* output:
* result1:false
* result2:true
*///~

gson 不能正确解析双花括号语法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
public class cronTest {
private static long count;

public static void main(String[] args) {

ArrayList<Item> items = getItemsWithDoubleBrackets();
System.out.println("getItemsWithDoubleBrackets:");
System.out.println(items);
System.out.println(new Gson().toJson(items));

System.out.println();

System.out.println("getItemsNormalWay:");
items = getItemsNormalWay();
System.out.println(items);
System.out.println(new Gson().toJson(items));
}

@NonNull
private static ArrayList<Item> getItemsWithDoubleBrackets() {
count = 0;
ArrayList<Item> items = new ArrayList<>();
for (int i = 0; i < 3; i++) {
items.add(new Item() {{ // double brackets
setContent("美好的一天" + count++);
}});
}
return items;
}

@NonNull
private static ArrayList<Item> getItemsNormalWay() {
count = 0;
ArrayList<Item> items = new ArrayList<>();
for (int i = 0; i < 3; i++) {
Item item = new Item();
item.setContent("美好的一天" + count++);
items.add(item);
}
return items;
}

static class Item {

private String content;

public void setContent(String content) {
this.content = content;
}

@Override
public String toString() {
return "{" +
"content='" + content + '\'' +
'}';
}
}
} /*
getItemsWithDoubleBrackets:
[{content='美好的一天0'}, {content='美好的一天1'}, {content='美好的一天2'}]
[null,null,null]

getItemsNormalWay:
[{content='美好的一天0'}, {content='美好的一天1'}, {content='美好的一天2'}]
[{"content":"美好的一天0"},{"content":"美好的一天1"},{"content":"美好的一天2"}]
*///~

原因是:添加的时候是匿名内部类,此匿名内部类不符合 gson 序列化规则,所以会是 null

image-20210809152411331image-20210809153711267

Gson doesn’t deserialise Long numbers correctly

RecordUtils.toRecord(proxyStocksOrders),让 flow_id 变成了重复的了,解决办法是用 Record 来逐个写;
如,ProxyDaoImpl.lock()方法里的 save() 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class RecordUtils {

/**
* 实体类转record
*/
public static Record toRecord(Object data) {

if (data == null) {
return null;
}

@SuppressWarnings("unchecked")
Map<String, Object> json = new Gson().fromJson(
new GsonBuilder()
.setFieldNamingStrategy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
.setDateFormat(CONST.FORMAT)
.create().toJson(data, data.getClass()),
HashMap.class
);
return new Record().setColumns(json);
}
}
class Main{
public static void main(String[] args) {
List<Record> records = new ArrayList<>();
List<Order> orders = new ArrayList<>();
for (int i = 0; i < 10; i++) {
Order order = new Order();
long flowId = Long.parseLong(Common.getPsoFlowId());
order.setFlowId(flowId);
order.setName(String.valueOf(flowId));

orders.add(order);
records.add(RecordUtils.toRecord(order));
}
System.out.println("----------");
records.forEach(System.out::println);
System.out.println("----------");
orders.forEach(System.out::println);
}
}

原因是,gson 将 long 类型的数据做了转换(如:把20200426163033383转换成了2.0200426163033384E16,把20200426163034384 也转换成了 2.0200426163034384E16
有一种解决办法是,对于这种 flow_id 这种类型的字段,用 string 类型,而不是 long 类型。

还有一种办法,添加:LongSerializationPolicy

1
2
3
4
5
6
// https://github.com/google/gson/issues/1084
new GsonBuilder()
.setFieldNamingStrategy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
.setDateFormat(CONST.FORMAT)
.setLongSerializationPolicy(LongSerializationPolicy.STRING) // add this line
.create().toJson(data, data.getClass()),

mybatis 语句拼接问题

1
2
3
4
5
6
7
8
9
/**
* 从 provider_marketable_goods 表中删除数据
*
* @param providerIds 拼接的 providerId
*/
@Update("<script>" +
"delete from goods where provider_id in (#{providerIds})" +
"</script>")
void removeGoodsByProviderId(@Param("providerIds") String providerIds);

实际的运行 sql 语句是: delete from goods where provider_id in (‘23,432,4432,4432’)

下面这个才是你想要的:

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 从 provider_marketable_goods 表中删除数据
*
* @param providerIds 拼接的 providerId
*/
@Update("<script>" +
"delete from goods where provider_id in (" +
"<foreach collection='providerIds' item='providerId' separator=','>" +
"#{providerId}" +
"</foreach>" +
")" +
"</script>")
void removeGoodsByProviderId(@Param("providerIds") Set<Long> providerIds);

实际的运行 sql 语句是: delete from goods where provider_id in (23,432,4432,4432)

作用域问题

搞错作用域,会导致获取的数据莫名其妙。
例如下面,本该放在循环内的,结果放在了循环之内(这种 bug 比较难找,要小心才是)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private void divideToHandleProviderIdToGoodsIdListMap(Consumer<Map<Long, List<Integer>>> consumer) {

Map<Long, List<Integer>> map = Maps.newHashMap(); // outside the loop
final int number = 100;
int count = 0;
List<ProviderGoods> list;
do {
list = providerMarketableDao.getProviderGoodsFromGoods(count * number, number);
list.stream().collect(Collectors.groupingBy(ProviderGoods::getProviderId))
.forEach((providerId, providerGoodsList) -> {
map.put(providerId, providerGoodsList.stream()
.map(ProviderGoods::getGoodsId)
.collect(Collectors.toList()));
});
consumer.accept(map);
count++;
} while (isNotEmptyList(list));
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private void divideToHandleProviderIdToGoodsIdListMap(Consumer<Map<Long, List<Integer>>> consumer) {

final int number = 100;
int count = 0;
List<ProviderGoods> list;
do {
Map<Long, List<Integer>> map = Maps.newHashMap(); // within the loop
list = providerMarketableDao.getProviderGoodsFromGoods(count * number, number);
list.stream().collect(Collectors.groupingBy(ProviderGoods::getProviderId))
.forEach((providerId, providerGoodsList) -> {
map.put(providerId, providerGoodsList.stream()
.map(ProviderGoods::getGoodsId)
.collect(Collectors.toList()));
});
consumer.accept(map);
count++;
} while (isNotEmptyList(list));
}

关于数据库‘状态’字段设计的思考与实践 - 倒骑的驴 - 博客园

KEY VALUE
CREATE_FAILED 创建订单失败(终态)
PAY_WAITTING 等待买家付款
PAY_CONFIRMING 付款确认中
PAY_FAILED 买家付款失败(终态,依赖需求而定)
PAY_SUCCESS 买家付款成功
DELIVERED 卖家已发货
RECEIVED 买家已收货
RETURNING 退货中
RETURN_SUCCESS 退货成功(终态)
CLOSED 订单关闭(终态)

拆表

order: 根据商家(provider_id)的维度来拆单;
order_item: 根据规格(product_id)的维度来拆单;

拆状态

STATUS: ‘PENDING’,’AUTO_CLOSED’,’CLOSED’,’REVIEW_FAIL’,’REVIEWED’,’PICK_FAIL’,’PICKED’,’FINISHED’

PAY_STATUS: ‘PENDING’,’PAYED’,’REFUND_PROCESSING’,’REFUND_SUCCESS’,’PARTIAL_REFUND’

SHIP_STATUS: ‘NOT_SHIPED’,’SENDING’,’DELIVERED’,’PARTIAL_RETURN’,’FULL_RETURN’,’PARTIAL_SENDING’

字段 类型 长度 小数点 不是 null 默认值 虚拟 注释
id bigint 20 0 notnull key
money decimal 10 2 notnull 0.00 和钱相关的
gmt_create datetime 0 0 current_timestamp
gmt_modified datetime 0 0 current_timestamp
is_disabled tinyint 1 0 是与否相关的,都用 tinyint
name varchar 64 0 和名称有关的,长度可选 32、64、123
intro text 0 0 比较长的文本,用 text 类型
start_time timestamp 0 0 需要的时间要很精确,用 timestamp 类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
function compose(a, b) {
let arr = [];
if (!a || a.length == 0) {
return b;
}

for (let i in a) {
for (let j in b) {
arr.push(a[i] + "," + b[j]);
}
}
return arr;
}

function getComposeResult(d) {
let result = compose([]);
for (let i = 0; i < d.length; i++) {
result = compose(
result,
d[i]
);
}
return result;
}

let a = [1, 2];
let b = [3, 4];
let c = [5, 6];
let d = [a, b, c];
// let d = [[1,2], [3,4], [5,6]]
console.log(getComposeResult(d));

// output: ["1,3,5", "1,3,6", "1,4,5", "1,4,6", "2,3,5", "2,3,6", "2,4,5", "2,4,6"]

扩展

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
let products = [{
"18547,49234,48480": 947858
}, {
"18547,49234,48481": 947859
}, {
"18547,49235,48480": 947860
}, {
"18547,49235,48481": 947861
}, {
"19167,49234,48480": 947862
}, {
"19167,49234,48481": 947863
}, {
"19167,49235,48480": 947864
}, {
"19167,49235,48481": 947865
}]

let spec = [{
"spec_name": "颜色",
"children": [{
"spec_value_name": "红",
"spec_value_image": "",
"spec_value_id": 18547,
"active": "selected",
"disabled": "false"
}, {
"spec_value_name": "白",
"spec_value_image": "",
"spec_value_id": 19167,
"disabled": "false"
}],
"spec_id": 4
}, {
"spec_name": "重量",
"children": [{
"spec_value_name": "1吨",
"spec_value_image": "",
"spec_value_id": 49234,
"active": "selected",
"disabled": "false"
}, {
"spec_value_name": "2吨",
"spec_value_image": "",
"spec_value_id": 49235,
"disabled": "false"
}],
"spec_id": 113
}, {
"spec_name": "大小",
"children": [{
"spec_value_name": "L",
"spec_value_image": "",
"spec_value_id": 48480,
"active": "selected",
"disabled": "false"
}, {
"spec_value_name": "M",
"spec_value_image": "",
"spec_value_id": 48481,
"disabled": "false"
}],
"spec_id": 742
}]

function getNumberArray(spec) {
let arr1 = []
for (let i in spec) {
let arr2 = [];
let children = spec[i].children;
for (let j in children) {
let child = children[j].spec_value_id;
arr2.push(child);
}
arr1.push(arr2);
}
return arr1;
}

function compose(a, b) {
let arr = [];
if (!a || a.length == 0) {
return b;
}

for (let i in a) {
for (let j in b) {
arr.push(a[i] + ',' + b[j])
}
}
return arr;
}

function getComposeSpecArray(numArr) {
let result = compose([])
for (let i = 0; i < numArr.length; i++) {
result = compose(result, numArr[i])
}
return result;
}

function getSelectSkuBySelectedStatus(spec) {
let arr = [];
for (let i in spec) {
for (let j in spec[i].children) {
let child = spec[i].children[j];
if (child.active && child.active == 'selected') {
arr.push(child.spec_value_id);
break;
}
}
}
return arr.join(',');
}

let arr = getNumberArray(spec);
console.log(arr)
// output: [[18547,19167],[49234,49235],[48480,48481]]

let specArr = getComposeSpecArray(arr)
console.log(specArr);
// output: ["18547,49234,48480", "18547,49234,48481", "18547,49235,48480", "18547,49235,48481", "19167,49234,48480", "19167,49234,48481", "19167,49235,48480", "19167,49235,48481"]

let sku = getSelectSkuBySelectedStatus(spec);
console.log(sku)
// output: '18547,49234,48480'
function getProductId(products, sku) {
for (let i in products) {
for (let j in products[i]) {
if (j == sku) {
return products[i][j];
}
}
return -1;
}
}
let productId = getProductId(products, sku)
console.log(productId)
// output: 947858
0%