Showing
5 changed files
with
352 additions
and
25 deletions
1 | +package com.yoho.search.service.restapi; | ||
2 | + | ||
3 | +import org.junit.Assert; | ||
4 | +import org.yaml.snakeyaml.Yaml; | ||
5 | + | ||
6 | +import java.io.FileInputStream; | ||
7 | +import java.net.URL; | ||
8 | +import java.util.HashMap; | ||
9 | +import java.util.List; | ||
10 | +import java.util.Map; | ||
11 | + | ||
12 | +/** | ||
13 | + * Created by ginozhang on 2016/11/17. | ||
14 | + */ | ||
15 | +public class ApiDefUtils { | ||
16 | + | ||
17 | + private static Map<String, Map<String, Object>> apiDefs = new HashMap<>(); | ||
18 | + | ||
19 | + public static Map<String, Object> getApiDef(String api) { | ||
20 | + | ||
21 | + if(apiDefs.containsKey(api)) | ||
22 | + { | ||
23 | + return apiDefs.get(api); | ||
24 | + } | ||
25 | + | ||
26 | + synchronized (ApiDefUtils.class) { | ||
27 | + if(apiDefs.containsKey(api)) | ||
28 | + { | ||
29 | + return apiDefs.get(api); | ||
30 | + } | ||
31 | + | ||
32 | + String fileName = api.substring(1).replace("/", "_"); | ||
33 | + Yaml yaml = new Yaml(); | ||
34 | + URL ymlUrl = SearchNewControllerTest.class.getClassLoader().getResource("api_def/" + fileName + ".yml"); | ||
35 | + Map<String, Object> def = null; | ||
36 | + try { | ||
37 | + def = (Map<String, Object>) yaml.load(new FileInputStream(ymlUrl.getFile())); | ||
38 | + } catch (Exception e) { | ||
39 | + throw new RuntimeException("cannot find file " + fileName, e); | ||
40 | + } | ||
41 | + | ||
42 | + Assert.assertNotNull(fileName, def); | ||
43 | + apiDefs.put(api, def); | ||
44 | + return def; | ||
45 | + } | ||
46 | + } | ||
47 | + | ||
48 | + public static List<String> getEnumItems(String api, String enumKey) | ||
49 | + { | ||
50 | + List<String> enums = ((Map<String, List<String>>)getApiDef(api).get("_enums")).get(enumKey); | ||
51 | + return enums; | ||
52 | + } | ||
53 | + | ||
54 | +} |
@@ -3,10 +3,8 @@ package com.yoho.search.service.restapi; | @@ -3,10 +3,8 @@ package com.yoho.search.service.restapi; | ||
3 | import com.alibaba.fastjson.JSONArray; | 3 | import com.alibaba.fastjson.JSONArray; |
4 | import com.alibaba.fastjson.JSONObject; | 4 | import com.alibaba.fastjson.JSONObject; |
5 | import org.junit.Assert; | 5 | import org.junit.Assert; |
6 | -import org.yaml.snakeyaml.Yaml; | ||
7 | 6 | ||
8 | -import java.io.FileInputStream; | ||
9 | -import java.net.URL; | 7 | +import java.util.Arrays; |
10 | import java.util.List; | 8 | import java.util.List; |
11 | import java.util.Map; | 9 | import java.util.Map; |
12 | import java.util.Set; | 10 | import java.util.Set; |
@@ -16,28 +14,19 @@ import java.util.Set; | @@ -16,28 +14,19 @@ import java.util.Set; | ||
16 | */ | 14 | */ |
17 | public class CheckStructureUtils { | 15 | public class CheckStructureUtils { |
18 | 16 | ||
19 | - public static void check(String fileName, JSONObject response) { | ||
20 | - Yaml yaml = new Yaml(); | ||
21 | - URL ymlUrl = SearchNewControllerTest.class.getClassLoader().getResource("api_def/" + fileName + ".yml"); | ||
22 | - Map<String, Object> def = null; | ||
23 | - try { | ||
24 | - def = (Map<String, Object>) yaml.load(new FileInputStream(ymlUrl.getFile())); | ||
25 | - } catch (Exception e) { | ||
26 | - throw new RuntimeException("cannot find file " + fileName, e); | ||
27 | - } | 17 | + public static void checkRequest(String api, String url) |
18 | + { | ||
19 | + // TODO: | ||
20 | + } | ||
28 | 21 | ||
29 | - Assert.assertNotNull(fileName, def); | 22 | + public static void check(Map<String, Object> apiDef, JSONObject response) { |
30 | Assert.assertTrue("The response should contain data.", response.containsKey("data") && response.get("data") != null); | 23 | Assert.assertTrue("The response should contain data.", response.containsKey("data") && response.get("data") != null); |
31 | - | ||
32 | - // check for repeat | ||
33 | - Map<String, Object> responseDef = (Map<String, Object>) def.get("_response"); | ||
34 | - System.out.println(def); | ||
35 | - Map<String, List<String>> enums = (Map<String, List<String>>) def.get("_enums"); | 24 | + Map<String, Object> responseDef = (Map<String, Object>) apiDef.get("_response"); |
25 | + Map<String, List<String>> enums = (Map<String, List<String>>) apiDef.get("_enums"); | ||
36 | doCheck(responseDef, response, enums); | 26 | doCheck(responseDef, response, enums); |
37 | } | 27 | } |
38 | 28 | ||
39 | private static void doCheck(Map<String, Object> responseDef, JSONObject response, Map<String, List<String>> enums) { | 29 | private static void doCheck(Map<String, Object> responseDef, JSONObject response, Map<String, List<String>> enums) { |
40 | - | ||
41 | if (responseDef == null || response.isEmpty()) { | 30 | if (responseDef == null || response.isEmpty()) { |
42 | return; | 31 | return; |
43 | } | 32 | } |
@@ -98,6 +87,13 @@ public class CheckStructureUtils { | @@ -98,6 +87,13 @@ public class CheckStructureUtils { | ||
98 | Assert.assertTrue("cannot find enum "+enumKey, enums.containsKey(enumKey)); | 87 | Assert.assertTrue("cannot find enum "+enumKey, enums.containsKey(enumKey)); |
99 | Assert.assertTrue("Invalid value "+sValue+" for enum "+enumKey, enums.get(enumKey).contains(sValue)); | 88 | Assert.assertTrue("Invalid value "+sValue+" for enum "+enumKey, enums.get(enumKey).contains(sValue)); |
100 | } | 89 | } |
90 | + else if(type.startsWith("connected_enum_")) | ||
91 | + { | ||
92 | + // 校验枚举类型 | ||
93 | + String enumKey = type.substring("connected_enum_".length()); | ||
94 | + Assert.assertTrue("cannot find enum "+enumKey, enums.containsKey(enumKey)); | ||
95 | + Assert.assertTrue("Invalid value "+sValue+" for enum "+enumKey, enums.get(enumKey).containsAll(Arrays.asList(sValue.split(",")))); | ||
96 | + } | ||
101 | else if(!"string".equalsIgnoreCase(type)) | 97 | else if(!"string".equalsIgnoreCase(type)) |
102 | { | 98 | { |
103 | Assert.fail("Unknown property type "+type); | 99 | Assert.fail("Unknown property type "+type); |
1 | +package com.yoho.search.service.restapi; | ||
2 | + | ||
3 | +import com.yoho.search.base.utils.FileUtils; | ||
4 | +import org.apache.commons.lang3.StringUtils; | ||
5 | + | ||
6 | +import java.util.HashMap; | ||
7 | +import java.util.Map; | ||
8 | + | ||
9 | +/** | ||
10 | + * Created by ginozhang on 2016/11/17. | ||
11 | + */ | ||
12 | +public class MarkDownUtils { | ||
13 | + | ||
14 | + private final String SEPARATOR = System.getProperty("line.separator"); | ||
15 | + | ||
16 | + private Map<String, String> typeDescptions = new HashMap<>(); | ||
17 | + | ||
18 | + private String api; | ||
19 | + | ||
20 | + private MarkDownUtils(String api) { | ||
21 | + this.api = api; | ||
22 | + } | ||
23 | + | ||
24 | + public static MarkDownUtils getInstance(String api) { | ||
25 | + return new MarkDownUtils(api); | ||
26 | + } | ||
27 | + | ||
28 | + { | ||
29 | + typeDescptions.put("connected_int", "int多值参数类型,多个值用逗号分隔。"); | ||
30 | + typeDescptions.put("ranged_double", "double范围类型,使用逗号分隔下限和上限"); | ||
31 | + typeDescptions.put("ranged_int", "int范围类型,使用逗号分隔下限和上限"); | ||
32 | + } | ||
33 | + | ||
34 | + | ||
35 | + /** | ||
36 | + * 根据接口描述文件生成接口描述的MD文件 | ||
37 | + */ | ||
38 | + public void generateApiMd() { | ||
39 | + Map<String, Object> apiDef = ApiDefUtils.getApiDef(api); | ||
40 | + StringBuffer sb = new StringBuffer(500); | ||
41 | + sb.append("# ").append(api).append(" 接口说明").append(SEPARATOR); | ||
42 | + | ||
43 | + // 1. 生成header 秒杀接口的功能和使用场景 | ||
44 | + sb.append(apiDef.get("_name")).append(SEPARATOR); | ||
45 | + | ||
46 | + // 2. 生成请求参数说明 | ||
47 | + sb.append(getRequestMdTable(apiDef)); | ||
48 | + | ||
49 | + // 3. 生成响应参数说明 | ||
50 | + sb.append(generateResponseMdTable(apiDef)); | ||
51 | + | ||
52 | + FileUtils.writeFile("test.md", sb.toString()); | ||
53 | + } | ||
54 | + | ||
55 | + private StringBuffer generateResponseMdTable(Map<String, Object> apiDef) { | ||
56 | + return generateMdTable4SingleObject(api + " 响应参数说明", (Map<String, Object>) apiDef.get("_response"), 2); | ||
57 | + } | ||
58 | + | ||
59 | + private StringBuffer generateMdTable4SingleObject(String tableTitle, Map<String, Object> respnseDef, int level) { | ||
60 | + StringBuffer sb = new StringBuffer(100); | ||
61 | + sb.append(getLevelChars(level)).append(tableTitle).append(SEPARATOR); | ||
62 | + sb.append("|参数名 |参数类型 |参数说明 |").append(SEPARATOR); | ||
63 | + sb.append("|------|--------|---------|").append(SEPARATOR); | ||
64 | + for (Map.Entry<String, Object> entry : respnseDef.entrySet()) { | ||
65 | + String paramName = entry.getKey(); | ||
66 | + Map<String, Object> details = ((Map<String, Object>) entry.getValue()); | ||
67 | + String type = getTypeDescription((String) details.get("_type")); | ||
68 | + String desc = (String) details.get("_desc"); | ||
69 | + sb.append("|").append(paramName).append("|").append(type).append("|").append(desc).append("|").append(SEPARATOR); | ||
70 | + | ||
71 | + if ("object".equals(type) || "array".equals(type)) { | ||
72 | + // 如果是数组或者对象类型 就新搞一个表格来说明里面的对象 | ||
73 | + sb.append(SEPARATOR).append(SEPARATOR); | ||
74 | + sb.append(generateMdTable4SingleObject("参数" + paramName + "的属性说明", (Map<String, Object>) details.get("_content"), level + 1)); | ||
75 | + } | ||
76 | + | ||
77 | + } | ||
78 | + sb.append(SEPARATOR).append(SEPARATOR); | ||
79 | + return sb; | ||
80 | + } | ||
81 | + | ||
82 | + private StringBuffer getRequestMdTable(Map<String, Object> apiDef) { | ||
83 | + StringBuffer sb = new StringBuffer(100); | ||
84 | + sb.append("## ").append(api).append(" 请求参数说明").append(SEPARATOR); | ||
85 | + sb.append("|参数名 |参数类型 |参数说明 |").append(SEPARATOR); | ||
86 | + sb.append("|------|--------|---------|").append(SEPARATOR); | ||
87 | + | ||
88 | + Map<String, Object> requestDef = (Map<String, Object>) apiDef.get("_request"); | ||
89 | + for (Map.Entry<String, Object> entry : requestDef.entrySet()) { | ||
90 | + String paramName = entry.getKey(); | ||
91 | + Map<String, String> details = ((Map<String, String>) entry.getValue()); | ||
92 | + String type = getTypeDescription(details.get("_type")); | ||
93 | + String desc = details.get("_desc"); | ||
94 | + sb.append("|").append(paramName).append("|").append(type).append("|").append(desc).append("|").append(SEPARATOR); | ||
95 | + } | ||
96 | + sb.append(SEPARATOR).append(SEPARATOR); | ||
97 | + return sb; | ||
98 | + } | ||
99 | + | ||
100 | + private StringBuffer getLevelChars(int level) { | ||
101 | + StringBuffer sb = new StringBuffer(10); | ||
102 | + level = level <= 3 ? level : 3; | ||
103 | + for (int i = 1; i <= level; i++) { | ||
104 | + sb.append("#"); | ||
105 | + } | ||
106 | + | ||
107 | + sb.append(" "); | ||
108 | + return sb; | ||
109 | + } | ||
110 | + | ||
111 | + private String getTypeDescription(String type) { | ||
112 | + | ||
113 | + if (StringUtils.isEmpty(type)) { | ||
114 | + type = "string"; | ||
115 | + } | ||
116 | + | ||
117 | + if (typeDescptions.containsKey(type)) { | ||
118 | + return typeDescptions.get(type); | ||
119 | + } | ||
120 | + | ||
121 | + if (type.startsWith("enum_")) { | ||
122 | + String enumKey = type.substring("enum_".length()); | ||
123 | + return "枚举类型,可选范围:" + ApiDefUtils.getEnumItems(api, enumKey); | ||
124 | + } else if (type.startsWith("connected_enum_")) { | ||
125 | + String enumKey = type.substring("connected_enum_".length()); | ||
126 | + return "多值枚举类型,枚举可选范围:" + ApiDefUtils.getEnumItems(api, enumKey) + ", 多个值用逗号分隔。"; | ||
127 | + } else if (type.startsWith("optional")) { | ||
128 | + String specialValue = type.substring("optional(".length(), "optional(".length() + 1); | ||
129 | + return "特定值{" + specialValue + "}或不传值"; | ||
130 | + } | ||
131 | + | ||
132 | + StringBuffer sb = new StringBuffer(type); | ||
133 | + return sb.toString(); | ||
134 | + } | ||
135 | + | ||
136 | +} |
@@ -75,6 +75,8 @@ public class SearchNewControllerTest { | @@ -75,6 +75,8 @@ public class SearchNewControllerTest { | ||
75 | if (appendParams.containsKey(api)) { | 75 | if (appendParams.containsKey(api)) { |
76 | url = url + "?" + appendParams.get(api); | 76 | url = url + "?" + appendParams.get(api); |
77 | } | 77 | } |
78 | + | ||
79 | + CheckStructureUtils.checkRequest(api, url); | ||
78 | String jsonResult = HttpClientUtils.getMethod(url); | 80 | String jsonResult = HttpClientUtils.getMethod(url); |
79 | System.out.println("Request url is: " + url + ". jsonResult: \n" + jsonResult); | 81 | System.out.println("Request url is: " + url + ". jsonResult: \n" + jsonResult); |
80 | JSONObject jsonObject = JSON.parseObject(jsonResult); | 82 | JSONObject jsonObject = JSON.parseObject(jsonResult); |
@@ -83,10 +85,10 @@ public class SearchNewControllerTest { | @@ -83,10 +85,10 @@ public class SearchNewControllerTest { | ||
83 | Assert.assertEquals(api, "200", jsonObject.getString("code")); | 85 | Assert.assertEquals(api, "200", jsonObject.getString("code")); |
84 | 86 | ||
85 | // 检查响应结果是否OK | 87 | // 检查响应结果是否OK |
86 | - String fileName = api.substring(1).replace("/", "_"); | ||
87 | - CheckStructureUtils.check(fileName, jsonObject); | 88 | + Map<String, Object> def = ApiDefUtils.getApiDef(api); |
89 | + CheckStructureUtils.check(def, jsonObject); | ||
88 | 90 | ||
89 | // 生成markdown接口说明文件 | 91 | // 生成markdown接口说明文件 |
90 | - | 92 | + MarkDownUtils.getInstance(api).generateApiMd(); |
91 | } | 93 | } |
92 | } | 94 | } |
@@ -4,15 +4,153 @@ | @@ -4,15 +4,153 @@ | ||
4 | # _desc: 字段描述 | 4 | # _desc: 字段描述 |
5 | # _required: 字段是否必须,默认为false | 5 | # _required: 字段是否必须,默认为false |
6 | # _type: 字段类型,可选为int、long、string、double、object、array、enum_<ENUM_NAME>,默认为string | 6 | # _type: 字段类型,可选为int、long、string、double、object、array、enum_<ENUM_NAME>,默认为string |
7 | +_name: 商品列表搜索接口。 | ||
7 | _enums: | 8 | _enums: |
8 | YesOrNo: ['Y','N'] | 9 | YesOrNo: ['Y','N'] |
9 | Status: ['0','1'] | 10 | Status: ['0','1'] |
10 | Gender: ['1','2','3'] | 11 | Gender: ['1','2','3'] |
11 | - Outlet: ['0','1', '2'] | 12 | + AgeLevel: ['1','2','3'] |
13 | + Outlet: ['1', '2'] | ||
14 | + VIPDiscountType: ['0', '1', '2'] | ||
15 | + Attribute: ['1', '2'] | ||
16 | + AppType: ['0', '1'] | ||
17 | + SellChannel: ['0', '1', '2'] | ||
12 | _request: | 18 | _request: |
13 | query: | 19 | query: |
14 | _desc: '搜索关键词' | 20 | _desc: '搜索关键词' |
15 | _type: 'string' | 21 | _type: 'string' |
22 | + viewNum: | ||
23 | + _desc: '分页每页显示商品数量' | ||
24 | + _type: 'int' | ||
25 | + page: | ||
26 | + _desc: '分页页数' | ||
27 | + _type: 'int' | ||
28 | + product_skn: | ||
29 | + _desc: '指定的商品SKN列表' | ||
30 | + _type: 'connected_int' | ||
31 | + brand: | ||
32 | + _desc: '指定的品牌列表' | ||
33 | + _type: 'connected_int' | ||
34 | + shop: | ||
35 | + _desc: '指定的店铺列表' | ||
36 | + _type: 'connected_int' | ||
37 | + msort: | ||
38 | + _desc: '指定的大分类列表' | ||
39 | + _type: 'connected_int' | ||
40 | + misort: | ||
41 | + _desc: '指定的中分类列表' | ||
42 | + _type: 'connected_int' | ||
43 | + sort: | ||
44 | + _desc: '指定的小分类列表' | ||
45 | + _type: 'connected_int' | ||
46 | + color: | ||
47 | + _desc: '指定的颜色列表' | ||
48 | + _type: 'connected_int' | ||
49 | + style: | ||
50 | + _desc: '指定的风格列表' | ||
51 | + _type: 'connected_int' | ||
52 | + size: | ||
53 | + _desc: '指定的尺寸列表' | ||
54 | + _type: 'connected_int' | ||
55 | + gender: | ||
56 | + _desc: '性别(1:男,2:女,3:通用)' | ||
57 | + _type: 'connected_enum_Gender' | ||
58 | + ageLevel: | ||
59 | + _desc: '年龄段(1成人 2大童 3小童)' | ||
60 | + _type: 'connected_enum_AgeLevel' | ||
61 | + price: | ||
62 | + _desc: '价格区间' | ||
63 | + _type: 'ranged_int' | ||
64 | + specialoffer: | ||
65 | + _desc: '是否为促销品[5折以下]' | ||
66 | + _type: 'enum_YesOrNo' | ||
67 | + isdiscount: | ||
68 | + _desc: '是否打折' | ||
69 | + _type: 'enum_YesOrNo' | ||
70 | + vdt: | ||
71 | + _desc: 'VIP折扣类型' | ||
72 | + _type: 'enum_VIPDiscountType' | ||
73 | + p_d: | ||
74 | + _desc: '折扣范围,浮点型,如【p_d_int=0.1,0.3】' | ||
75 | + _type: 'ranged_double' | ||
76 | + p_d_int: | ||
77 | + _desc: '折扣范围,整形,如【p_d_int=1,3】' | ||
78 | + _type: 'ranged_int' | ||
79 | + isStudentPrice: | ||
80 | + _desc: '是否有学生价优惠' | ||
81 | + _type: 'enum_YesOrNo' | ||
82 | + isStudentRebate: | ||
83 | + _desc: '是否有学生返币' | ||
84 | + _type: 'enum_YesOrNo' | ||
85 | + isInstalment: | ||
86 | + _desc: '是否分期' | ||
87 | + _type: 'enum_Status' | ||
88 | + sales: | ||
89 | + _desc: '是否在售' | ||
90 | + _type: 'enum_Status' | ||
91 | + promotion: | ||
92 | + _desc: '是否促销/推广商品 TODO' | ||
93 | + _type: 'int' | ||
94 | + attribute: | ||
95 | + _desc: '包含指定的商品属性(1:正常商品,2:赠品)' | ||
96 | + _type: 'enum_Attribute' | ||
97 | + attribute: | ||
98 | + _desc: '过滤指定的商品属性(1:正常商品,2:赠品)' | ||
99 | + _type: 'enum_Attribute' | ||
100 | + limited: | ||
101 | + _desc: '是否限量商品' | ||
102 | + _type: 'enum_YesOrNo' | ||
103 | + new: | ||
104 | + _desc: '是否新品' | ||
105 | + _type: 'enum_YesOrNo' | ||
106 | + outlets: | ||
107 | + _desc: '是否奥莱商品' | ||
108 | + _type: 'enum_Outlet' | ||
109 | + status: | ||
110 | + _desc: '是否上架' | ||
111 | + _type: 'enum_Status' | ||
112 | + breaking: | ||
113 | + _desc: '传入1只查询断码商品,不传值或其他值该参数不起作用' | ||
114 | + _type: 'optional(1)' | ||
115 | + app_type: | ||
116 | + _desc: 'APP类型' | ||
117 | + _type: 'connected_enum_AppType' | ||
118 | + sell_channels: | ||
119 | + _desc: '销售平台 (网站、APP、现场 TODO)' | ||
120 | + _type: 'enum_SellChannel' | ||
121 | + stocknumber: | ||
122 | + _desc: '商品库存,当传入0时查询库存为0的商品,当传入大于0的数值时,查询库存不小于改数值的商品' | ||
123 | + _type: 'int' | ||
124 | + folder_id: | ||
125 | + _desc: '商品目录' | ||
126 | + _type: 'int' | ||
127 | + series_id: | ||
128 | + _desc: '商品系列' | ||
129 | + _type: 'int' | ||
130 | + first_shelve_time: | ||
131 | + _desc: '首次上架时间间隔' | ||
132 | + _type: 'ranged_double' | ||
133 | + shelve_time: | ||
134 | + _desc: '最近上架日期间隔' | ||
135 | + _type: 'ranged_double' | ||
136 | + day: | ||
137 | + _desc: '上架时间' | ||
138 | + _type: 'date(yyyy-MM-dd)' | ||
139 | + contain_global: | ||
140 | + _desc: '传入Y查询结果包含全球购商品,不传值或其他值该参数不起作用' | ||
141 | + _type: 'optional(Y)' | ||
142 | + contain_seckill: | ||
143 | + _desc: '传入Y查询结果包含秒杀商品,不传值或其他值该参数不起作用' | ||
144 | + _type: 'optional(Y)' | ||
145 | + act_temp: | ||
146 | + _desc: '活动属性-模板id' | ||
147 | + _type: 'int' | ||
148 | + act_rec: | ||
149 | + _desc: '活动属性-是否推荐' | ||
150 | + _type: 'enum_Status' | ||
151 | + act_status: | ||
152 | + _desc: '活动属性-状态' | ||
153 | + _type: 'enum_Status' | ||
16 | _response: | 154 | _response: |
17 | code: | 155 | code: |
18 | _desc: '响应状态码,200为成功,其他为失败' | 156 | _desc: '响应状态码,200为成功,其他为失败' |
@@ -159,10 +297,11 @@ _response: | @@ -159,10 +297,11 @@ _response: | ||
159 | _desc: '是否是奥特莱斯商品' | 297 | _desc: '是否是奥特莱斯商品' |
160 | _type: 'enum_Outlet' | 298 | _type: 'enum_Outlet' |
161 | gender: | 299 | gender: |
162 | - _desc: '性别' | 300 | + _desc: '性别(1:男,2:女,3:通用)' |
163 | _type: 'enum_Gender' | 301 | _type: 'enum_Gender' |
164 | age_level: | 302 | age_level: |
165 | - _desc: '商品适合的年龄层' | 303 | + _desc: '年龄段(1成人 2大童 3小童)' |
304 | + _type: 'connected_enum_AgeLevel' | ||
166 | default_images: | 305 | default_images: |
167 | _desc: '默认的商品图片' | 306 | _desc: '默认的商品图片' |
168 | yohood_id: | 307 | yohood_id: |
-
Please register or login to post a comment