Compare commits
25 Commits
9b2a072147
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| b7f6016abb | |||
| 15a4c49b06 | |||
| f2af26326c | |||
| 62730da767 | |||
| b2c89a2c69 | |||
| cc0bdace42 | |||
| 227766c431 | |||
| c81208e3c8 | |||
| 418b89c1ae | |||
| 9efeae1fed | |||
| ef286a2466 | |||
| 2943bf0320 | |||
| 3ef1b8331b | |||
| abe8e8ce5b | |||
| 9d1fdd8669 | |||
| 0f83de521f | |||
| 6bb7f33864 | |||
| c8da895533 | |||
| d7763927ac | |||
| 191f6799a0 | |||
| 937d88c9aa | |||
| 766a972af1 | |||
| 59d07a4c71 | |||
| bdb1d7b8e1 | |||
| 5edb069bd5 |
772
1.json
Normal file
772
1.json
Normal file
@@ -0,0 +1,772 @@
|
|||||||
|
首页 | 编辑页面 | 查询分析器 | 手机号码批量解密
|
||||||
|
选择API并发送请求
|
||||||
|
FLXG3D56 - 特殊名单验证
|
||||||
|
FLXG54F5 - 易诉人群识别
|
||||||
|
FLXG162A - 团伙欺诈排查(通用版)
|
||||||
|
FLXG970F - 风险人员核验
|
||||||
|
FLXG5876 - 易诉人
|
||||||
|
FLXG9687 - 电诈风险预警-标准版
|
||||||
|
FLXGC9D1 - 黑灰产等级
|
||||||
|
FLXGCA3D - 个人综合涉诉
|
||||||
|
FLXGDEC7 - 个人不良
|
||||||
|
FLXG0V3B - 个人不良标准版
|
||||||
|
FLXG0V4B - 个人司法涉诉(详版)
|
||||||
|
IVYZ385E - 自然人生存状态标识
|
||||||
|
IVYZ5733 - 单人婚姻登记信息核验
|
||||||
|
IVYZ9363 - 双人婚姻状态识别
|
||||||
|
JRZQ0A03 - 借贷意向验证
|
||||||
|
JRZQ4AA8 - 偿债压力指数
|
||||||
|
JRZQ8203 - 借贷行为验证
|
||||||
|
JRZQDCBE - 银行卡四要素验证
|
||||||
|
QYGL2ACD - 企业三要素核验
|
||||||
|
QYGL6F2D - 人企关联
|
||||||
|
QYGL45BD - 企业法人四要素核验
|
||||||
|
QYGL8261 - 企业综合涉诉
|
||||||
|
QYGL8271 - 企业司法涉诉(详版)
|
||||||
|
QYGLB4C0 - 股东人企关系精准版
|
||||||
|
YYSY4B37 - 手机在网时长
|
||||||
|
YYSY4B21 - 手机在网状态
|
||||||
|
YYSY6F2E - 运营商三要素核验(详版)
|
||||||
|
YYSY09CD - 运营商三要素验证(简版)
|
||||||
|
YYSYBE08 - 运营商二要素核验(姓名、身份证)
|
||||||
|
YYSYD50F - 运营商二要素核验(手机号、身份证)
|
||||||
|
YYSYF7DB - 手机二次卡
|
||||||
|
IVYZ9A2B - 学历信息查询
|
||||||
|
COMB298Y - 人事背调组合包
|
||||||
|
COMB86PM - 海之源科技定制组合包
|
||||||
|
传入参数(自动加密) 传入加密内容
|
||||||
|
使用 Mock 数据(仅学历查询接口有效)
|
||||||
|
开启后将使用预设的 Mock 数据,不会发送实际请求
|
||||||
|
请求超时时间:
|
||||||
|
60
|
||||||
|
秒
|
||||||
|
{
|
||||||
|
"responses": [
|
||||||
|
{
|
||||||
|
"api_code": "FLXG0687",
|
||||||
|
"data": {
|
||||||
|
"value": [
|
||||||
|
{
|
||||||
|
"riskLevel": "0",
|
||||||
|
"riskType": "110"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"riskLevel": "0",
|
||||||
|
"riskType": "170"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"riskLevel": "1",
|
||||||
|
"riskType": "150"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"riskLevel": "0",
|
||||||
|
"riskType": "130"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"success": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"api_code": "FLXG54F5",
|
||||||
|
"data": {
|
||||||
|
"code": "1004",
|
||||||
|
"data": {
|
||||||
|
"msg": "无接口的请求权限",
|
||||||
|
"code": "1004",
|
||||||
|
"orderNo": "20250725130737834774411",
|
||||||
|
"data": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"success": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"api_code": "JRZQ8203",
|
||||||
|
"data": {
|
||||||
|
"code": "00",
|
||||||
|
"data": {
|
||||||
|
"tl_id_t11_nbank_org": "1",
|
||||||
|
"tl_id_m6_nbank_passnum": "7",
|
||||||
|
"tl_cell_t2_nbank_lendamt": "71",
|
||||||
|
"tl_cell_t10_nbank_num": "2",
|
||||||
|
"tl_cell_t5_nbank_org": "2",
|
||||||
|
"tl_id_t5_nbank_num": "3",
|
||||||
|
"tl_id_t0_nbank_org": "4",
|
||||||
|
"tl_cell_t9_nbank_num": "2",
|
||||||
|
"tl_id_t10_nbank_lendamt": "14",
|
||||||
|
"tl_cell_eletail_lasttime": "2025-05-26",
|
||||||
|
"tl_id_t7_nbank_org": "1",
|
||||||
|
"tl_id_m1_nbank_passlendamt": "11",
|
||||||
|
"tl_cell_m9_nbank_passnum": "8",
|
||||||
|
"flag_datastrategy": "1",
|
||||||
|
"tl_cell_m6_nbank_passorg": "3",
|
||||||
|
"tl_cell_m1_nbank_passorg": "1",
|
||||||
|
"tl_id_m12_nbank_passorg": "4",
|
||||||
|
"tl_id_m3_nbank_passnum": "4",
|
||||||
|
"DataStrategy": {
|
||||||
|
"strategy_version": "1.0",
|
||||||
|
"product_type": "",
|
||||||
|
"strategy_id": "DTA_BR0008250",
|
||||||
|
"product_name": "预置_借贷行为验证",
|
||||||
|
"scene": "lend"
|
||||||
|
},
|
||||||
|
"tl_id_t6_nbank_lendamt": "31",
|
||||||
|
"tl_id_t6_nbank_org": "1",
|
||||||
|
"tl_cell_m12_nbank_passlendamt": "81",
|
||||||
|
"tl_cell_m3_nbank_passlendamt": "25",
|
||||||
|
"tl_cell_t7_nbank_lendamt": "27",
|
||||||
|
"tl_id_t10_nbank_num": "2",
|
||||||
|
"tl_cell_m12_nbank_passnum": "9",
|
||||||
|
"tl_cell_t2_nbank_num": "8",
|
||||||
|
"tl_cell_t6_nbank_org": "1",
|
||||||
|
"tl_cell_t11_nbank_num": "2",
|
||||||
|
"tl_id_m3_nbank_passlendamt": "25",
|
||||||
|
"tl_cell_m6_nbank_passnum": "7",
|
||||||
|
"tl_id_t7_nbank_lendamt": "27",
|
||||||
|
"tl_cell_t11_nbank_org": "1",
|
||||||
|
"tl_cell_t10_nbank_lendamt": "14",
|
||||||
|
"tl_id_m3_nbank_passorg": "2",
|
||||||
|
"tl_cell_t8_nbank_num": "2",
|
||||||
|
"tl_id_t6_nbank_num": "4",
|
||||||
|
"tl_id_eletail_org": "1",
|
||||||
|
"tl_id_eletail_lasttime": "2025-05-26",
|
||||||
|
"tl_id_t11_nbank_reamt": "1",
|
||||||
|
"tl_id_t10_nbank_org": "1",
|
||||||
|
"tl_cell_m12_nbank_passorg": "4",
|
||||||
|
"tl_id_t6_nbank_reamt": "8",
|
||||||
|
"tl_id_t7_nbank_reamt": "6",
|
||||||
|
"tl_id_t3_nbank_num": "6",
|
||||||
|
"tl_cell_t8_nbank_lendamt": "18",
|
||||||
|
"tl_id_t9_nbank_org": "1",
|
||||||
|
"tl_id_t1_nbank_lendamt": "70",
|
||||||
|
"tl_cell_t7_nbank_org": "1",
|
||||||
|
"tl_id_t9_nbank_reamt": "3",
|
||||||
|
"tl_id_t8_nbank_reamt": "3",
|
||||||
|
"tl_id_t1_nbank_org": "4",
|
||||||
|
"tl_cell_t3_nbank_num": "6",
|
||||||
|
"tl_id_t8_nbank_lendamt": "18",
|
||||||
|
"tl_cell_t11_nbank_lendamt": "10",
|
||||||
|
"tl_id_t10_nbank_reamt": "2",
|
||||||
|
"tl_cell_m3_nbank_passorg": "2",
|
||||||
|
"tl_id_t0_nbank_num": "9",
|
||||||
|
"tl_cell_t4_nbank_org": "2",
|
||||||
|
"tl_cell_eletail_num": "1",
|
||||||
|
"tl_cell_t0_nbank_num": "9",
|
||||||
|
"tl_id_t0_nbank_lendamt": "81",
|
||||||
|
"tl_cell_t1_nbank_org": "4",
|
||||||
|
"tl_id_t9_nbank_lendamt": "18",
|
||||||
|
"tl_cell_t9_nbank_lendamt": "18",
|
||||||
|
"tl_cell_t0_nbank_lendamt": "81",
|
||||||
|
"tl_id_t1_nbank_num": "8",
|
||||||
|
"tl_cell_t0_nbank_reamt": "48",
|
||||||
|
"tl_cell_t1_nbank_reamt": "48",
|
||||||
|
"tl_id_t5_nbank_lendamt": "25",
|
||||||
|
"tl_cell_m3_nbank_passnum": "4",
|
||||||
|
"tl_id_m6_nbank_passlendamt": "60",
|
||||||
|
"tl_cell_t6_nbank_lendamt": "31",
|
||||||
|
"tl_id_m1_nbank_passorg": "1",
|
||||||
|
"tl_cell_t6_nbank_num": "4",
|
||||||
|
"tl_id_t8_nbank_num": "2",
|
||||||
|
"tl_id_t4_nbank_org": "2",
|
||||||
|
"tl_cell_t1_nbank_lendamt": "70",
|
||||||
|
"tl_cell_t2_nbank_org": "4",
|
||||||
|
"tl_cell_t3_nbank_lendamt": "57",
|
||||||
|
"tl_id_t2_nbank_num": "8",
|
||||||
|
"tl_cell_t5_nbank_num": "3",
|
||||||
|
"swift_number": "3034309_20250725130737_902618FDA19",
|
||||||
|
"tl_id_t5_nbank_reamt": "10",
|
||||||
|
"tl_cell_t7_nbank_reamt": "6",
|
||||||
|
"tl_cell_t9_nbank_org": "1",
|
||||||
|
"tl_cell_t9_nbank_reamt": "3",
|
||||||
|
"tl_id_t4_nbank_reamt": "12",
|
||||||
|
"tl_id_t3_nbank_reamt": "37",
|
||||||
|
"tl_cell_t8_nbank_reamt": "3",
|
||||||
|
"tl_cell_t4_nbank_reamt": "12",
|
||||||
|
"tl_id_t1_nbank_reamt": "48",
|
||||||
|
"tl_id_t2_nbank_reamt": "48",
|
||||||
|
"tl_cell_t5_nbank_reamt": "10",
|
||||||
|
"tl_cell_t3_nbank_reamt": "37",
|
||||||
|
"tl_cell_t6_nbank_reamt": "8",
|
||||||
|
"tl_cell_m9_nbank_passorg": "3",
|
||||||
|
"tl_cell_t2_nbank_reamt": "48",
|
||||||
|
"tl_cell_m9_nbank_passlendamt": "72",
|
||||||
|
"tl_cell_t11_nbank_reamt": "1",
|
||||||
|
"tl_id_m12_nbank_passlendamt": "81",
|
||||||
|
"tl_cell_t10_nbank_reamt": "2",
|
||||||
|
"tl_id_t9_nbank_num": "2",
|
||||||
|
"tl_id_m9_nbank_passorg": "3",
|
||||||
|
"tl_id_t0_nbank_reamt": "48",
|
||||||
|
"tl_id_t2_nbank_lendamt": "71",
|
||||||
|
"tl_id_t3_nbank_org": "3",
|
||||||
|
"tl_cell_t4_nbank_num": "4",
|
||||||
|
"tl_cell_t3_nbank_org": "3",
|
||||||
|
"tl_cell_m1_nbank_passnum": "2",
|
||||||
|
"code": "00",
|
||||||
|
"tl_id_t2_nbank_org": "4",
|
||||||
|
"tl_id_m12_nbank_passnum": "9",
|
||||||
|
"tl_cell_t4_nbank_lendamt": "29",
|
||||||
|
"tl_id_m9_nbank_passlendamt": "72",
|
||||||
|
"tl_cell_eletail_org": "1",
|
||||||
|
"tl_cell_t0_nbank_org": "4",
|
||||||
|
"tl_id_m9_nbank_passnum": "8",
|
||||||
|
"tl_id_t3_nbank_lendamt": "57",
|
||||||
|
"tl_cell_m1_nbank_passlendamt": "11",
|
||||||
|
"tl_cell_t1_nbank_num": "8",
|
||||||
|
"tl_id_m6_nbank_passorg": "3",
|
||||||
|
"tl_cell_t10_nbank_org": "1",
|
||||||
|
"tl_id_t11_nbank_lendamt": "10",
|
||||||
|
"tl_id_t11_nbank_num": "2",
|
||||||
|
"tl_cell_t5_nbank_lendamt": "25",
|
||||||
|
"tl_id_t5_nbank_org": "2",
|
||||||
|
"tl_cell_eletail_lasttype": "a",
|
||||||
|
"tl_id_t4_nbank_lendamt": "29",
|
||||||
|
"tl_cell_t8_nbank_org": "1",
|
||||||
|
"tl_id_t7_nbank_num": "3",
|
||||||
|
"tl_cell_m6_nbank_passlendamt": "60",
|
||||||
|
"tl_id_t4_nbank_num": "4",
|
||||||
|
"tl_id_m1_nbank_passnum": "2",
|
||||||
|
"tl_id_t8_nbank_org": "1",
|
||||||
|
"tl_cell_t7_nbank_num": "3",
|
||||||
|
"flag_totalloan": "1",
|
||||||
|
"tl_id_eletail_lasttype": "a",
|
||||||
|
"tl_id_eletail_num": "1"
|
||||||
|
},
|
||||||
|
"flag_totalloan": "1"
|
||||||
|
},
|
||||||
|
"success": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"api_code": "FLXG3D56",
|
||||||
|
"data": {
|
||||||
|
"code": "100002",
|
||||||
|
"flag_specialList_c": "0",
|
||||||
|
"data": {
|
||||||
|
"code": "100002",
|
||||||
|
"flag_specialList_c": "0",
|
||||||
|
"swift_number": "3034309_20250725130737_902518FDA19",
|
||||||
|
"flag_datastrategy": "0",
|
||||||
|
"DataStrategy": {
|
||||||
|
"strategy_version": "1.0",
|
||||||
|
"product_type": "100099",
|
||||||
|
"strategy_id": "DTA_BR0007511",
|
||||||
|
"product_name": "预置_特殊名单验证",
|
||||||
|
"scene": "lend"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"success": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"api_code": "JRZQ0A03",
|
||||||
|
"data": {
|
||||||
|
"code": "00",
|
||||||
|
"data": {
|
||||||
|
"als_m1_cell_nbank_allnum": "3",
|
||||||
|
"als_d15_cell_nbank_week_allnum": "0",
|
||||||
|
"als_d7_cell_bank_allnum": "1",
|
||||||
|
"als_m12_cell_nbank_nsloan_orgnum": "1",
|
||||||
|
"als_m6_id_bank_tra_allnum": "2",
|
||||||
|
"als_m6_cell_nbank_max_inteday": "45",
|
||||||
|
"als_d15_cell_nbank_sloan_allnum": "1",
|
||||||
|
"als_m12_id_bank_selfnum": "0",
|
||||||
|
"als_m12_cell_nbank_ca_allnum": "1",
|
||||||
|
"als_m6_id_pdl_allnum": "2",
|
||||||
|
"als_m3_cell_min_monnum": "1",
|
||||||
|
"als_m1_cell_bank_tra_allnum": "1",
|
||||||
|
"als_m3_id_bank_tra_allnum": "2",
|
||||||
|
"als_m6_cell_nbank_orgnum": "6",
|
||||||
|
"als_m12_id_bank_min_monnum": "0",
|
||||||
|
"als_m12_id_bank_allnum": "2",
|
||||||
|
"als_d15_id_rel_orgnum": "2",
|
||||||
|
"als_m12_cell_nbank_sloan_orgnum": "2",
|
||||||
|
"als_m1_id_nbank_allnum": "3",
|
||||||
|
"als_m6_cell_nbank_max_monnum": "4",
|
||||||
|
"als_m12_cell_caon_allnum": "5",
|
||||||
|
"als_m6_cell_nbank_night_allnum": "4",
|
||||||
|
"als_m3_cell_nbank_orgnum": "3",
|
||||||
|
"als_m3_cell_min_inteday": "0",
|
||||||
|
"als_m6_cell_nbank_sloan_allnum": "5",
|
||||||
|
"als_m12_id_nbank_max_monnum": "4",
|
||||||
|
"als_m1_cell_nbank_sloan_allnum": "2",
|
||||||
|
"als_m3_id_min_inteday": "0",
|
||||||
|
"als_d15_cell_bank_orgnum": "1",
|
||||||
|
"als_m12_cell_bank_orgnum": "2",
|
||||||
|
"als_m1_cell_nbank_night_orgnum": "0",
|
||||||
|
"als_m1_id_nbank_cf_orgnum": "2",
|
||||||
|
"als_d15_cell_bank_selfnum": "0",
|
||||||
|
"als_m6_cell_bank_tra_allnum": "2",
|
||||||
|
"als_lst_cell_bank_csinteday": "1",
|
||||||
|
"als_d15_cell_bank_allnum": "1",
|
||||||
|
"als_d15_cell_nbank_night_orgnum": "0",
|
||||||
|
"als_m6_cell_nbank_night_orgnum": "4",
|
||||||
|
"als_m12_cell_nbank_nsloan_allnum": "1",
|
||||||
|
"als_m12_cell_nbank_max_inteday": "62",
|
||||||
|
"als_m6_cell_max_monnum": "4",
|
||||||
|
"als_d15_cell_nbank_night_allnum": "0",
|
||||||
|
"als_m12_cell_min_monnum": "0",
|
||||||
|
"als_m12_cell_bank_allnum": "2",
|
||||||
|
"als_m3_id_nbank_max_monnum": "4",
|
||||||
|
"als_m3_cell_max_monnum": "4",
|
||||||
|
"als_m3_id_bank_min_monnum": "0",
|
||||||
|
"als_m6_cell_bank_tra_orgnum": "2",
|
||||||
|
"als_m1_cell_nbank_night_allnum": "0",
|
||||||
|
"als_m3_cell_nbank_allnum": "7",
|
||||||
|
"als_d15_cell_bank_week_orgnum": "0",
|
||||||
|
"als_m3_id_bank_tra_orgnum": "2",
|
||||||
|
"als_d15_cell_rel_orgnum": "2",
|
||||||
|
"als_d15_cell_nbank_cf_allnum": "1",
|
||||||
|
"als_m6_cell_nbank_oth_allnum": "7",
|
||||||
|
"als_m6_cell_nbank_selfnum": "0",
|
||||||
|
"als_m12_cell_nbank_sloan_allnum": "10",
|
||||||
|
"als_m12_id_nbank_cons_orgnum": "4",
|
||||||
|
"als_m3_cell_bank_max_monnum": "1",
|
||||||
|
"als_m12_cell_nbank_night_orgnum": "4",
|
||||||
|
"als_m6_id_nbank_nsloan_allnum": "1",
|
||||||
|
"als_m3_id_nbank_oth_allnum": "1",
|
||||||
|
"als_m12_id_bank_orgnum": "2",
|
||||||
|
"als_d7_cell_rel_orgnum": "1",
|
||||||
|
"als_d7_cell_bank_orgnum": "1",
|
||||||
|
"swift_number": "3034309_20250725130737_79792FE4A19",
|
||||||
|
"als_m12_id_tot_mons": "11",
|
||||||
|
"als_m12_id_avg_monnum": "1.91",
|
||||||
|
"als_d7_cell_bank_week_orgnum": "0",
|
||||||
|
"als_m3_id_nbank_sloan_orgnum": "1",
|
||||||
|
"als_m3_cell_nbank_max_inteday": "45",
|
||||||
|
"als_d7_cell_bank_week_allnum": "0",
|
||||||
|
"als_m1_id_nbank_orgnum": "2",
|
||||||
|
"als_m3_id_bank_selfnum": "0",
|
||||||
|
"als_m6_id_bank_tra_orgnum": "2",
|
||||||
|
"als_d15_cell_bank_night_allnum": "0",
|
||||||
|
"als_m1_cell_nbank_orgnum": "2",
|
||||||
|
"als_m6_id_bank_selfnum": "0",
|
||||||
|
"als_m6_id_max_monnum": "4",
|
||||||
|
"als_m12_cell_bank_selfnum": "0",
|
||||||
|
"als_m3_cell_nbank_selfnum": "0",
|
||||||
|
"als_m3_id_nbank_oth_orgnum": "1",
|
||||||
|
"als_m12_id_nbank_ca_allnum": "1",
|
||||||
|
"als_m1_cell_nbank_cons_allnum": "1",
|
||||||
|
"als_m3_id_nbank_max_inteday": "45",
|
||||||
|
"als_d15_id_nbank_selfnum": "0",
|
||||||
|
"als_m12_cell_nbank_max_monnum": "4",
|
||||||
|
"als_m6_cell_min_monnum": "1",
|
||||||
|
"als_d15_cell_rel_allnum": "2",
|
||||||
|
"als_m6_id_bank_avg_monnum": "1.00",
|
||||||
|
"als_m12_id_nbank_selfnum": "0",
|
||||||
|
"als_d15_cell_bank_week_allnum": "0",
|
||||||
|
"als_m6_cell_nbank_oth_orgnum": "4",
|
||||||
|
"als_m1_cell_nbank_cons_orgnum": "1",
|
||||||
|
"als_m6_id_pdl_orgnum": "2",
|
||||||
|
"als_lst_id_nbank_consnum": "1",
|
||||||
|
"als_m6_id_rel_allnum": "6",
|
||||||
|
"als_m6_id_rel_orgnum": "2",
|
||||||
|
"als_m12_id_nbank_oth_allnum": "7",
|
||||||
|
"als_m12_id_cooff_orgnum": "1",
|
||||||
|
"als_m6_cell_tot_mons": "6",
|
||||||
|
"als_m12_id_bank_tra_allnum": "2",
|
||||||
|
"als_m12_id_nbank_ca_orgnum": "1",
|
||||||
|
"als_m3_id_max_inteday": "42",
|
||||||
|
"als_d7_id_bank_night_allnum": "0",
|
||||||
|
"als_m3_cell_nbank_sloan_orgnum": "1",
|
||||||
|
"als_m1_id_nbank_night_orgnum": "0",
|
||||||
|
"als_m12_id_bank_tra_orgnum": "2",
|
||||||
|
"als_m6_cell_nbank_nsloan_allnum": "1",
|
||||||
|
"als_m6_cell_pdl_allnum": "2",
|
||||||
|
"flag_datastrategy": "1",
|
||||||
|
"als_m1_id_bank_orgnum": "1",
|
||||||
|
"als_m3_cell_pdl_allnum": "1",
|
||||||
|
"als_m1_id_bank_selfnum": "0",
|
||||||
|
"als_m12_cell_nbank_cf_allnum": "11",
|
||||||
|
"als_d7_id_bank_selfnum": "0",
|
||||||
|
"als_m3_id_rel_orgnum": "2",
|
||||||
|
"als_m6_id_nbank_min_inteday": "0",
|
||||||
|
"als_m3_id_caon_orgnum": "1",
|
||||||
|
"als_m6_id_nbank_nsloan_orgnum": "1",
|
||||||
|
"als_m6_cell_nbank_cons_allnum": "8",
|
||||||
|
"als_m1_cell_nbank_week_allnum": "0",
|
||||||
|
"als_m1_id_nbank_sloan_allnum": "2",
|
||||||
|
"als_d15_id_bank_night_allnum": "0",
|
||||||
|
"als_d15_id_bank_allnum": "1",
|
||||||
|
"als_m1_id_nbank_cf_allnum": "3",
|
||||||
|
"als_lst_cell_nbank_inteday": "8",
|
||||||
|
"als_m1_cell_bank_week_orgnum": "0",
|
||||||
|
"als_m3_cell_bank_week_allnum": "0",
|
||||||
|
"als_lst_id_bank_inteday": "2",
|
||||||
|
"als_m6_id_max_inteday": "42",
|
||||||
|
"als_m12_cell_rel_orgnum": "2",
|
||||||
|
"als_m1_id_bank_allnum": "1",
|
||||||
|
"als_d7_cell_rel_allnum": "1",
|
||||||
|
"als_m12_cell_nbank_min_monnum": "0",
|
||||||
|
"als_m6_id_tot_mons": "6",
|
||||||
|
"als_m1_id_bank_week_allnum": "0",
|
||||||
|
"als_m12_cell_bank_max_monnum": "1",
|
||||||
|
"als_m3_cell_tot_mons": "3",
|
||||||
|
"als_m12_id_rel_allnum": "10",
|
||||||
|
"als_m3_id_bank_max_inteday": "58",
|
||||||
|
"als_m3_id_rel_allnum": "5",
|
||||||
|
"als_m12_cell_nbank_selfnum": "0",
|
||||||
|
"als_m3_cell_bank_week_orgnum": "0",
|
||||||
|
"als_m12_cell_tot_mons": "11",
|
||||||
|
"als_m1_cell_bank_week_allnum": "0",
|
||||||
|
"als_m12_id_rel_orgnum": "2",
|
||||||
|
"als_lst_cell_bank_inteday": "2",
|
||||||
|
"als_m6_id_nbank_min_monnum": "0",
|
||||||
|
"als_d15_id_bank_night_orgnum": "0",
|
||||||
|
"als_m12_id_bank_max_inteday": "58",
|
||||||
|
"als_m6_id_bank_min_monnum": "0",
|
||||||
|
"als_m3_id_nbank_tot_mons": "2",
|
||||||
|
"als_m3_id_bank_orgnum": "2",
|
||||||
|
"als_m3_id_bank_week_allnum": "0",
|
||||||
|
"als_m3_cell_nbank_night_orgnum": "1",
|
||||||
|
"als_m6_cell_nbank_sloan_orgnum": "1",
|
||||||
|
"als_m1_id_nbank_sloan_orgnum": "1",
|
||||||
|
"als_m1_cell_bank_allnum": "1",
|
||||||
|
"als_m1_id_rel_allnum": "3",
|
||||||
|
"als_m12_cell_max_monnum": "4",
|
||||||
|
"als_m12_cell_nbank_allnum": "19",
|
||||||
|
"als_m1_cell_bank_night_allnum": "0",
|
||||||
|
"als_d15_id_nbank_week_orgnum": "0",
|
||||||
|
"als_m1_id_nbank_night_allnum": "0",
|
||||||
|
"als_m12_cell_rel_allnum": "10",
|
||||||
|
"als_lst_id_bank_csinteday": "1",
|
||||||
|
"als_m1_id_bank_week_orgnum": "0",
|
||||||
|
"als_m3_id_bank_tot_mons": "2",
|
||||||
|
"als_d15_cell_nbank_sloan_orgnum": "1",
|
||||||
|
"als_m12_cell_bank_tra_allnum": "2",
|
||||||
|
"als_m3_id_bank_max_monnum": "1",
|
||||||
|
"als_m3_id_nbank_selfnum": "0",
|
||||||
|
"als_m1_cell_nbank_week_orgnum": "0",
|
||||||
|
"als_d15_id_nbank_sloan_allnum": "1",
|
||||||
|
"als_m3_cell_pdl_orgnum": "1",
|
||||||
|
"als_d15_id_nbank_sloan_orgnum": "1",
|
||||||
|
"als_m6_cell_nbank_allnum": "14",
|
||||||
|
"als_m3_cell_nbank_cf_allnum": "6",
|
||||||
|
"als_m1_id_nbank_week_allnum": "0",
|
||||||
|
"als_m6_cell_pdl_orgnum": "2",
|
||||||
|
"als_m12_cell_bank_tra_orgnum": "2",
|
||||||
|
"als_d15_id_rel_allnum": "2",
|
||||||
|
"als_m6_id_bank_tot_mons": "2",
|
||||||
|
"als_m3_id_tot_mons": "3",
|
||||||
|
"als_m6_cell_nbank_nsloan_orgnum": "1",
|
||||||
|
"als_m12_id_bank_week_allnum": "0",
|
||||||
|
"als_lst_id_nbank_inteday": "8",
|
||||||
|
"als_m6_id_nbank_selfnum": "0",
|
||||||
|
"als_m6_id_min_monnum": "1",
|
||||||
|
"code": "00",
|
||||||
|
"als_m12_cell_nbank_ca_orgnum": "1",
|
||||||
|
"als_d15_id_bank_selfnum": "0",
|
||||||
|
"als_m3_cell_bank_tra_orgnum": "2",
|
||||||
|
"als_d15_id_nbank_week_allnum": "0",
|
||||||
|
"als_fst_id_bank_inteday": "60",
|
||||||
|
"als_m3_id_avg_monnum": "3.00",
|
||||||
|
"als_d15_id_nbank_night_orgnum": "0",
|
||||||
|
"als_m1_cell_bank_selfnum": "0",
|
||||||
|
"als_m3_cell_nbank_night_allnum": "1",
|
||||||
|
"als_d15_id_nbank_night_allnum": "0",
|
||||||
|
"als_m12_cell_nbank_orgnum": "7",
|
||||||
|
"als_m3_cell_nbank_sloan_allnum": "4",
|
||||||
|
"als_lst_id_nbank_csinteday": "1",
|
||||||
|
"als_m3_id_bank_week_orgnum": "0",
|
||||||
|
"als_m12_cell_nbank_avg_monnum": "1.90",
|
||||||
|
"als_m3_id_nbank_allnum": "7",
|
||||||
|
"als_m3_cell_bank_tra_allnum": "2",
|
||||||
|
"als_m1_cell_bank_night_orgnum": "0",
|
||||||
|
"als_m6_id_nbank_allnum": "14",
|
||||||
|
"als_m12_cell_nbank_cf_orgnum": "2",
|
||||||
|
"als_m6_cell_nbank_min_monnum": "0",
|
||||||
|
"als_m1_cell_bank_orgnum": "1",
|
||||||
|
"als_m12_id_max_monnum": "4",
|
||||||
|
"als_m12_cell_bank_week_allnum": "0",
|
||||||
|
"als_d15_id_nbank_allnum": "1",
|
||||||
|
"als_m12_cell_bank_min_inteday": "58",
|
||||||
|
"als_m12_cell_bank_night_orgnum": "1",
|
||||||
|
"als_m12_id_min_monnum": "0",
|
||||||
|
"als_m12_id_bank_week_orgnum": "0",
|
||||||
|
"als_m1_id_bank_night_allnum": "0",
|
||||||
|
"als_m3_id_max_monnum": "4",
|
||||||
|
"als_m3_cell_nbank_oth_orgnum": "1",
|
||||||
|
"als_m12_cell_min_inteday": "0",
|
||||||
|
"als_d15_cell_nbank_selfnum": "0",
|
||||||
|
"als_d7_id_rel_allnum": "1",
|
||||||
|
"als_m3_id_nbank_orgnum": "3",
|
||||||
|
"als_m3_cell_bank_max_inteday": "58",
|
||||||
|
"als_lst_id_bank_consnum": "1",
|
||||||
|
"als_m1_id_nbank_week_orgnum": "0",
|
||||||
|
"als_m6_id_nbank_orgnum": "6",
|
||||||
|
"als_m1_id_nbank_cons_allnum": "1",
|
||||||
|
"als_fst_cell_bank_inteday": "60",
|
||||||
|
"als_m6_id_avg_monnum": "2.67",
|
||||||
|
"als_m3_cell_nbank_cons_orgnum": "2",
|
||||||
|
"als_m3_id_bank_night_allnum": "1",
|
||||||
|
"DataStrategy": {
|
||||||
|
"strategy_version": "1.0",
|
||||||
|
"product_type": "100099",
|
||||||
|
"strategy_id": "DTA_BR0007512",
|
||||||
|
"product_name": "预置_借贷意向验证",
|
||||||
|
"scene": "lend"
|
||||||
|
},
|
||||||
|
"als_m6_cell_bank_week_orgnum": "0",
|
||||||
|
"als_m3_cell_bank_night_orgnum": "1",
|
||||||
|
"als_m3_cell_nbank_cf_orgnum": "2",
|
||||||
|
"als_m6_id_bank_night_orgnum": "1",
|
||||||
|
"als_m3_id_nbank_cons_orgnum": "2",
|
||||||
|
"als_m1_id_bank_tra_orgnum": "1",
|
||||||
|
"als_fst_id_nbank_inteday": "352",
|
||||||
|
"als_m1_cell_nbank_cf_allnum": "3",
|
||||||
|
"als_m12_id_nbank_night_allnum": "5",
|
||||||
|
"als_m3_cell_caon_orgnum": "1",
|
||||||
|
"als_m12_id_nbank_avg_monnum": "1.90",
|
||||||
|
"als_d15_cell_nbank_orgnum": "1",
|
||||||
|
"als_m1_id_nbank_cons_orgnum": "1",
|
||||||
|
"als_m12_id_nbank_cf_allnum": "11",
|
||||||
|
"als_m12_cell_bank_tot_mons": "2",
|
||||||
|
"als_m12_id_nbank_sloan_allnum": "10",
|
||||||
|
"als_m3_cell_nbank_oth_allnum": "1",
|
||||||
|
"als_m6_id_nbank_oth_allnum": "7",
|
||||||
|
"als_m3_id_nbank_cons_allnum": "3",
|
||||||
|
"als_m12_cell_nbank_cons_orgnum": "4",
|
||||||
|
"als_m3_id_nbank_cf_allnum": "6",
|
||||||
|
"als_m12_cell_bank_avg_monnum": "1.00",
|
||||||
|
"als_m3_cell_bank_night_allnum": "1",
|
||||||
|
"als_m6_cell_nbank_avg_monnum": "2.80",
|
||||||
|
"als_m3_cell_bank_orgnum": "2",
|
||||||
|
"als_m3_cell_caon_allnum": "2",
|
||||||
|
"als_d15_id_bank_orgnum": "1",
|
||||||
|
"als_d7_cell_bank_night_allnum": "0",
|
||||||
|
"als_m6_id_nbank_cf_orgnum": "2",
|
||||||
|
"als_m3_id_bank_allnum": "2",
|
||||||
|
"als_m3_id_nbank_cf_orgnum": "2",
|
||||||
|
"als_m12_id_nbank_night_orgnum": "4",
|
||||||
|
"als_d7_cell_bank_night_orgnum": "0",
|
||||||
|
"als_m3_id_nbank_avg_monnum": "3.50",
|
||||||
|
"als_m3_id_min_monnum": "1",
|
||||||
|
"als_lst_cell_nbank_csinteday": "1",
|
||||||
|
"als_m1_id_bank_night_orgnum": "0",
|
||||||
|
"als_m1_id_rel_orgnum": "2",
|
||||||
|
"als_m1_cell_nbank_cf_orgnum": "2",
|
||||||
|
"als_m6_id_nbank_oth_orgnum": "4",
|
||||||
|
"als_m12_id_nbank_orgnum": "7",
|
||||||
|
"als_m3_id_nbank_night_allnum": "1",
|
||||||
|
"als_m3_cell_bank_selfnum": "0",
|
||||||
|
"als_m3_cell_nbank_cons_allnum": "3",
|
||||||
|
"als_m3_id_bank_min_inteday": "58",
|
||||||
|
"als_m6_cell_caon_orgnum": "3",
|
||||||
|
"als_m12_id_bank_avg_monnum": "1.00",
|
||||||
|
"als_m6_id_bank_night_allnum": "1",
|
||||||
|
"als_m6_cell_bank_tot_mons": "2",
|
||||||
|
"als_m3_id_caon_allnum": "2",
|
||||||
|
"als_m12_id_min_inteday": "0",
|
||||||
|
"als_m12_id_nbank_sloan_orgnum": "2",
|
||||||
|
"als_m12_id_pdl_orgnum": "2",
|
||||||
|
"als_m3_id_pdl_orgnum": "1",
|
||||||
|
"als_m1_id_bank_tra_allnum": "1",
|
||||||
|
"als_d15_id_nbank_orgnum": "1",
|
||||||
|
"als_m6_cell_nbank_cf_orgnum": "2",
|
||||||
|
"als_m12_cell_bank_night_allnum": "1",
|
||||||
|
"als_d7_id_bank_night_orgnum": "0",
|
||||||
|
"als_m6_cell_nbank_tot_mons": "5",
|
||||||
|
"als_d15_id_bank_tra_allnum": "1",
|
||||||
|
"als_m6_cell_cooff_orgnum": "1",
|
||||||
|
"als_m12_cell_bank_week_orgnum": "0",
|
||||||
|
"als_m3_cell_bank_allnum": "2",
|
||||||
|
"als_m6_cell_nbank_cons_orgnum": "4",
|
||||||
|
"als_d15_id_bank_tra_orgnum": "1",
|
||||||
|
"als_d7_id_bank_orgnum": "1",
|
||||||
|
"als_m12_id_nbank_cf_orgnum": "2",
|
||||||
|
"als_d15_cell_nbank_allnum": "1",
|
||||||
|
"als_m12_id_max_inteday": "62",
|
||||||
|
"als_m6_cell_bank_selfnum": "0",
|
||||||
|
"als_m1_cell_nbank_selfnum": "0",
|
||||||
|
"als_m12_id_cooff_allnum": "4",
|
||||||
|
"als_m6_id_nbank_night_allnum": "4",
|
||||||
|
"als_d15_id_bank_week_orgnum": "0",
|
||||||
|
"als_m1_cell_caon_orgnum": "1",
|
||||||
|
"als_m6_id_nbank_avg_monnum": "2.80",
|
||||||
|
"als_m12_id_bank_tot_mons": "2",
|
||||||
|
"als_m6_cell_nbank_cf_allnum": "7",
|
||||||
|
"als_m6_id_nbank_sloan_orgnum": "1",
|
||||||
|
"als_m1_cell_rel_allnum": "3",
|
||||||
|
"als_m3_cell_bank_tot_mons": "2",
|
||||||
|
"als_m12_id_nbank_max_inteday": "62",
|
||||||
|
"als_m6_cell_max_inteday": "42",
|
||||||
|
"als_m6_id_caon_allnum": "4",
|
||||||
|
"als_m6_id_nbank_night_orgnum": "4",
|
||||||
|
"als_m3_cell_cooff_orgnum": "1",
|
||||||
|
"als_m6_id_bank_max_inteday": "58",
|
||||||
|
"als_m12_id_bank_min_inteday": "58",
|
||||||
|
"als_m3_cell_nbank_max_monnum": "4",
|
||||||
|
"als_m6_id_nbank_sloan_allnum": "5",
|
||||||
|
"als_m1_cell_rel_orgnum": "2",
|
||||||
|
"als_m6_cell_bank_min_inteday": "58",
|
||||||
|
"als_m12_cell_nbank_min_inteday": "0",
|
||||||
|
"als_m3_cell_cooff_allnum": "1",
|
||||||
|
"als_d15_id_bank_week_allnum": "0",
|
||||||
|
"als_m6_id_caon_orgnum": "3",
|
||||||
|
"als_d7_id_bank_allnum": "1",
|
||||||
|
"als_m6_cell_cooff_allnum": "4",
|
||||||
|
"als_m3_id_cooff_allnum": "1",
|
||||||
|
"als_m6_cell_min_inteday": "0",
|
||||||
|
"als_m6_id_cooff_allnum": "4",
|
||||||
|
"als_m1_cell_caon_allnum": "1",
|
||||||
|
"als_m12_id_caon_orgnum": "4",
|
||||||
|
"als_m12_id_nbank_nsloan_allnum": "1",
|
||||||
|
"als_m6_cell_nbank_week_orgnum": "4",
|
||||||
|
"als_m3_cell_rel_orgnum": "2",
|
||||||
|
"als_m6_cell_rel_orgnum": "2",
|
||||||
|
"als_m12_id_nbank_oth_orgnum": "4",
|
||||||
|
"als_m12_id_nbank_cons_allnum": "8",
|
||||||
|
"als_m3_id_nbank_sloan_allnum": "4",
|
||||||
|
"als_m12_cell_avg_monnum": "1.91",
|
||||||
|
"als_d15_id_nbank_cf_allnum": "1",
|
||||||
|
"als_m3_cell_max_inteday": "42",
|
||||||
|
"als_m12_cell_cooff_allnum": "4",
|
||||||
|
"als_m3_id_bank_avg_monnum": "1.00",
|
||||||
|
"flag_applyloanstr": "1",
|
||||||
|
"als_m6_cell_bank_night_allnum": "1",
|
||||||
|
"als_m6_id_bank_week_orgnum": "0",
|
||||||
|
"als_m12_id_nbank_week_orgnum": "5",
|
||||||
|
"als_m3_cell_nbank_tot_mons": "2",
|
||||||
|
"als_d7_cell_bank_selfnum": "0",
|
||||||
|
"als_m3_cell_bank_avg_monnum": "1.00",
|
||||||
|
"als_d15_cell_bank_night_orgnum": "0",
|
||||||
|
"als_d15_cell_bank_tra_orgnum": "1",
|
||||||
|
"als_m12_cell_nbank_week_allnum": "8",
|
||||||
|
"als_d15_cell_bank_tra_allnum": "1",
|
||||||
|
"als_m1_id_caon_allnum": "1",
|
||||||
|
"als_m12_id_nbank_week_allnum": "8",
|
||||||
|
"als_m6_cell_bank_min_monnum": "0",
|
||||||
|
"als_m3_cell_nbank_min_inteday": "0",
|
||||||
|
"als_m12_id_nbank_allnum": "19",
|
||||||
|
"als_m3_cell_avg_monnum": "3.00",
|
||||||
|
"als_m6_id_nbank_tot_mons": "5",
|
||||||
|
"als_m12_cell_nbank_oth_orgnum": "4",
|
||||||
|
"als_m3_cell_bank_min_inteday": "58",
|
||||||
|
"als_m12_cell_pdl_allnum": "2",
|
||||||
|
"als_m6_cell_caon_allnum": "4",
|
||||||
|
"als_m12_id_nbank_tot_mons": "10",
|
||||||
|
"als_m12_id_pdl_allnum": "2",
|
||||||
|
"als_m6_id_cooff_orgnum": "1",
|
||||||
|
"als_m3_id_cooff_orgnum": "1",
|
||||||
|
"als_m3_id_nbank_night_orgnum": "1",
|
||||||
|
"als_m12_id_nbank_min_inteday": "0",
|
||||||
|
"als_m6_cell_bank_allnum": "2",
|
||||||
|
"als_m6_cell_nbank_week_allnum": "6",
|
||||||
|
"als_m12_id_caon_allnum": "5",
|
||||||
|
"als_m12_cell_pdl_orgnum": "2",
|
||||||
|
"als_m6_cell_bank_max_monnum": "1",
|
||||||
|
"als_m3_id_nbank_min_inteday": "0",
|
||||||
|
"als_m6_cell_bank_max_inteday": "58",
|
||||||
|
"als_d7_cell_bank_tra_allnum": "1",
|
||||||
|
"als_m6_cell_bank_orgnum": "2",
|
||||||
|
"als_m12_cell_cooff_orgnum": "1",
|
||||||
|
"als_m3_id_pdl_allnum": "1",
|
||||||
|
"als_m12_cell_nbank_oth_allnum": "7",
|
||||||
|
"als_d15_id_nbank_cf_orgnum": "1",
|
||||||
|
"als_m6_cell_avg_monnum": "2.67",
|
||||||
|
"als_m12_cell_nbank_night_allnum": "5",
|
||||||
|
"als_m6_id_bank_min_inteday": "58",
|
||||||
|
"als_d15_cell_nbank_cf_orgnum": "1",
|
||||||
|
"als_m6_id_nbank_week_allnum": "6",
|
||||||
|
"als_m6_cell_bank_night_orgnum": "1",
|
||||||
|
"als_m1_cell_bank_tra_orgnum": "1",
|
||||||
|
"als_m1_id_nbank_selfnum": "0",
|
||||||
|
"als_m1_id_caon_orgnum": "1",
|
||||||
|
"als_m3_cell_bank_min_monnum": "0",
|
||||||
|
"als_m6_id_nbank_cf_allnum": "7",
|
||||||
|
"als_m6_id_bank_max_monnum": "1",
|
||||||
|
"als_m6_id_nbank_week_orgnum": "4",
|
||||||
|
"als_m6_id_bank_orgnum": "2",
|
||||||
|
"als_m6_cell_bank_avg_monnum": "1.00",
|
||||||
|
"als_m3_cell_nbank_min_monnum": "0",
|
||||||
|
"als_d7_cell_bank_tra_orgnum": "1",
|
||||||
|
"als_m6_id_min_inteday": "0",
|
||||||
|
"als_lst_cell_bank_consnum": "1",
|
||||||
|
"als_m6_id_nbank_max_inteday": "45",
|
||||||
|
"als_m12_cell_caon_orgnum": "4",
|
||||||
|
"als_d15_cell_nbank_week_orgnum": "0",
|
||||||
|
"als_m6_cell_bank_week_allnum": "0",
|
||||||
|
"als_m1_cell_nbank_sloan_orgnum": "1",
|
||||||
|
"als_m3_cell_rel_allnum": "5",
|
||||||
|
"als_m6_id_nbank_max_monnum": "4",
|
||||||
|
"als_m12_id_nbank_nsloan_orgnum": "1",
|
||||||
|
"als_m12_cell_nbank_cons_allnum": "8",
|
||||||
|
"als_m12_cell_nbank_tot_mons": "10",
|
||||||
|
"als_d7_id_bank_tra_allnum": "1",
|
||||||
|
"als_m6_cell_rel_allnum": "6",
|
||||||
|
"als_m12_cell_nbank_week_orgnum": "5",
|
||||||
|
"als_d7_id_rel_orgnum": "1",
|
||||||
|
"als_m6_id_bank_allnum": "2",
|
||||||
|
"als_m6_id_bank_week_allnum": "0",
|
||||||
|
"als_m6_id_nbank_cons_orgnum": "4",
|
||||||
|
"als_m3_id_nbank_week_orgnum": "1",
|
||||||
|
"als_d7_id_bank_week_allnum": "0",
|
||||||
|
"als_m12_id_bank_night_allnum": "1",
|
||||||
|
"als_m3_id_bank_night_orgnum": "1",
|
||||||
|
"als_m3_id_nbank_min_monnum": "0",
|
||||||
|
"als_d7_id_bank_tra_orgnum": "1",
|
||||||
|
"als_m12_id_bank_max_monnum": "1",
|
||||||
|
"als_fst_cell_nbank_inteday": "352",
|
||||||
|
"als_m3_id_nbank_week_allnum": "1",
|
||||||
|
"als_m6_id_nbank_cons_allnum": "8",
|
||||||
|
"als_m12_cell_max_inteday": "62",
|
||||||
|
"als_d7_id_bank_week_orgnum": "0",
|
||||||
|
"als_m6_cell_nbank_min_inteday": "0",
|
||||||
|
"als_m12_id_nbank_min_monnum": "0",
|
||||||
|
"als_lst_cell_nbank_consnum": "1",
|
||||||
|
"als_m3_cell_nbank_week_orgnum": "1",
|
||||||
|
"als_m12_cell_bank_max_inteday": "58",
|
||||||
|
"als_m12_cell_bank_min_monnum": "0",
|
||||||
|
"als_m3_cell_nbank_avg_monnum": "3.50",
|
||||||
|
"als_m12_id_bank_night_orgnum": "1",
|
||||||
|
"als_m3_cell_nbank_week_allnum": "1"
|
||||||
|
},
|
||||||
|
"flag_applyloanstr": "1"
|
||||||
|
},
|
||||||
|
"success": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"api_code": "YYSY6F2E",
|
||||||
|
"data": {
|
||||||
|
"code": "1000",
|
||||||
|
"data": {
|
||||||
|
"msg": "一致",
|
||||||
|
"phoneType": "CMCC",
|
||||||
|
"code": 1000,
|
||||||
|
"guid": "ac185429175342005772573521997520250725130737725sg3YR",
|
||||||
|
"encryptType": "MD5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"success": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"api_code": "IVYZ5733",
|
||||||
|
"data": {
|
||||||
|
"code": "0",
|
||||||
|
"data": {
|
||||||
|
"data": "INR:匹配不成功"
|
||||||
|
},
|
||||||
|
"seqNo": "96TZ31B9250725130737829",
|
||||||
|
"message": "成功"
|
||||||
|
},
|
||||||
|
"success": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"api_code": "FLXG9687",
|
||||||
|
"data": {
|
||||||
|
"code": "00",
|
||||||
|
"tfps_level": "0",
|
||||||
|
"swift_number": "3034309_20250725130737_79782FE4A19",
|
||||||
|
"flag_telefraudpredictstd": "1"
|
||||||
|
},
|
||||||
|
"success": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"api_code": "FLXG0V4B",
|
||||||
|
"data": {
|
||||||
|
"data": "{\"sxbzxr\":[{\"msg\":\"没有找到\",\"sxbzxr\":\"[]\"}],\"xgbzxr\":[{\"msg\":\"没有找到\",\"xgbzxr\":\"[]\"}],\"entout\":[{\"msg\":\"没有找到\",\"entout\":\"{}\"}]}",
|
||||||
|
"success": true,
|
||||||
|
"resultCode": "200",
|
||||||
|
"message": "调用成功"
|
||||||
|
},
|
||||||
|
"success": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
449
API调用流程域设计.md
Normal file
449
API调用流程域设计.md
Normal file
@@ -0,0 +1,449 @@
|
|||||||
|
# API 调用流程域设计分析
|
||||||
|
|
||||||
|
## 🎯 业务流程分析
|
||||||
|
|
||||||
|
根据你描述的场景,这是一个典型的**B2B 数据服务 API 调用流程**:
|
||||||
|
|
||||||
|
```
|
||||||
|
用户企业 → 调用我们的产品API → 获取第三方大数据服务 → 计费扣款
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🏗️ 涉及的业务域分析
|
||||||
|
|
||||||
|
这个流程总共涉及 **6 个核心域**:
|
||||||
|
|
||||||
|
### 1. 🚪 **网关域 (Gateway Domain)**
|
||||||
|
|
||||||
|
- **职责**: 统一入口、基础路由、请求预处理
|
||||||
|
- **功能**:
|
||||||
|
- 接收所有外部请求
|
||||||
|
- 基础的请求验证
|
||||||
|
- 路由到具体的业务域
|
||||||
|
|
||||||
|
### 2. 🛡️ **安全域 (Security Domain)**
|
||||||
|
|
||||||
|
- **职责**: 认证授权、加密解密、白名单管理
|
||||||
|
- **功能**:
|
||||||
|
- IP 白名单验证
|
||||||
|
- 企业 ID 验证
|
||||||
|
- 密钥管理
|
||||||
|
- 请求参数解密
|
||||||
|
|
||||||
|
### 3. 👤 **用户域 (User Domain)**
|
||||||
|
|
||||||
|
- **职责**: 用户和企业信息管理
|
||||||
|
- **功能**:
|
||||||
|
- 企业认证信息验证
|
||||||
|
- 用户权限检查
|
||||||
|
- 企业密钥获取
|
||||||
|
|
||||||
|
### 4. 📦 **产品域 (Product Domain)**
|
||||||
|
|
||||||
|
- **职责**: 产品访问控制、权限管理
|
||||||
|
- **功能**:
|
||||||
|
- 产品访问权限验证
|
||||||
|
- API 限额检查
|
||||||
|
- 产品配置管理
|
||||||
|
|
||||||
|
### 5. 📊 **数据服务域 (Data Service Domain)**
|
||||||
|
|
||||||
|
- **职责**: 核心业务逻辑、第三方 API 调用
|
||||||
|
- **功能**:
|
||||||
|
- 参数处理和转换
|
||||||
|
- 调用上游数据公司 API
|
||||||
|
- 数据格式转换和响应
|
||||||
|
|
||||||
|
### 6. 💰 **计费域 (Billing Domain)**
|
||||||
|
|
||||||
|
- **职责**: 计费扣款、账单管理
|
||||||
|
- **功能**:
|
||||||
|
- 钱包余额检查
|
||||||
|
- 费用计算
|
||||||
|
- 扣款操作
|
||||||
|
- 计费记录
|
||||||
|
|
||||||
|
### 7. 📋 **审计域 (Audit Domain)**
|
||||||
|
|
||||||
|
- **职责**: 请求记录、日志管理
|
||||||
|
- **功能**:
|
||||||
|
- API 调用记录
|
||||||
|
- 操作日志
|
||||||
|
- 审计追踪
|
||||||
|
|
||||||
|
## 🔄 完整流程设计
|
||||||
|
|
||||||
|
### 架构图
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────┐
|
||||||
|
│ 客户端 │
|
||||||
|
└─────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ 网关域 (Gateway) │
|
||||||
|
│ • 请求接收 • 基础验证 • 路由分发 • 响应聚合 │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ 数据服务域 (Data Service) │
|
||||||
|
│ [主要协调者] │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
│ │ │ │ │ │
|
||||||
|
▼ ▼ ▼ ▼ ▼ ▼
|
||||||
|
┌────────┐┌────────┐┌────────┐┌────────┐┌────────┐┌────────┐
|
||||||
|
│安全域 ││用户域 ││产品域 ││计费域 ││审计域 ││第三方 │
|
||||||
|
│Security││User ││Product ││Billing ││Audit ││API │
|
||||||
|
└────────┘└────────┘└────────┘└────────┘└────────┘└────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 详细流程设计
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 完整的API调用流程
|
||||||
|
func (h *DataServiceHandler) ProcessAPIRequest(ctx context.Context, req *APIRequest) (*APIResponse, error) {
|
||||||
|
// 1. 审计域 - 开始记录
|
||||||
|
auditID := h.auditService.StartRequest(ctx, req)
|
||||||
|
defer h.auditService.EndRequest(ctx, auditID)
|
||||||
|
|
||||||
|
// 2. 安全域 - IP白名单验证
|
||||||
|
if err := h.securityService.ValidateIP(ctx, req.ClientIP); err != nil {
|
||||||
|
return nil, errors.Wrap(err, "IP not in whitelist")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 用户域 - 企业ID验证
|
||||||
|
enterprise, err := h.userService.GetEnterpriseByID(ctx, req.EnterpriseID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "invalid enterprise ID")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 安全域 - 获取密钥并解密参数
|
||||||
|
secretKey, err := h.securityService.GetEnterpriseSecret(ctx, req.EnterpriseID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "failed to get enterprise secret")
|
||||||
|
}
|
||||||
|
|
||||||
|
decryptedParams, err := h.securityService.DecryptParams(ctx, req.EncryptedParams, secretKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "failed to decrypt parameters")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 产品域 - 产品权限验证
|
||||||
|
if err := h.productService.ValidateAccess(ctx, enterprise.ID, req.ProductCode); err != nil {
|
||||||
|
return nil, errors.Wrap(err, "no access to product")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. 计费域 - 检查余额
|
||||||
|
cost, err := h.billingService.CalculateCost(ctx, req.ProductCode, decryptedParams)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "failed to calculate cost")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.billingService.CheckBalance(ctx, enterprise.ID, cost); err != nil {
|
||||||
|
return nil, errors.Wrap(err, "insufficient balance")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. 数据服务域 - 调用第三方API
|
||||||
|
upstreamResp, err := h.callUpstreamAPI(ctx, decryptedParams)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "upstream API call failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 8. 计费域 - 扣费
|
||||||
|
if err := h.billingService.ChargeAccount(ctx, enterprise.ID, cost, auditID); err != nil {
|
||||||
|
// 记录扣费失败,但不影响响应
|
||||||
|
h.logger.Error("charge failed", zap.Error(err), zap.String("audit_id", auditID))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 9. 安全域 - 加密响应
|
||||||
|
encryptedResp, err := h.securityService.EncryptResponse(ctx, upstreamResp, secretKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "failed to encrypt response")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &APIResponse{
|
||||||
|
Data: encryptedResp,
|
||||||
|
RequestID: auditID,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚪 路由入口设计
|
||||||
|
|
||||||
|
### 1. 网关层路由配置
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# gateway.yaml
|
||||||
|
routes:
|
||||||
|
- path: /api/v1/data/*
|
||||||
|
service: data-service
|
||||||
|
middlewares:
|
||||||
|
- request-id # 生成请求ID
|
||||||
|
- rate-limit # 基础限流
|
||||||
|
- audit-start # 开始审计
|
||||||
|
timeout: 60s
|
||||||
|
|
||||||
|
- path: /api/v1/admin/*
|
||||||
|
service: admin-service
|
||||||
|
middlewares: [admin-auth, rate-limit]
|
||||||
|
|
||||||
|
- path: /api/v1/user/*
|
||||||
|
service: user-service
|
||||||
|
middlewares: [user-auth, rate-limit]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 网关中间件
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 网关层的请求预处理中间件
|
||||||
|
func DataAPIMiddleware() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
// 1. 生成请求ID
|
||||||
|
requestID := uuid.New().String()
|
||||||
|
c.Set("request_id", requestID)
|
||||||
|
c.Header("X-Request-ID", requestID)
|
||||||
|
|
||||||
|
// 2. 提取企业ID(从Header或路径参数)
|
||||||
|
enterpriseID := extractEnterpriseID(c)
|
||||||
|
if enterpriseID == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "missing enterprise ID"})
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Set("enterprise_id", enterpriseID)
|
||||||
|
|
||||||
|
// 3. 提取客户端IP
|
||||||
|
clientIP := c.ClientIP()
|
||||||
|
c.Set("client_ip", clientIP)
|
||||||
|
|
||||||
|
// 4. 提取产品代码
|
||||||
|
productCode := c.Param("product_code")
|
||||||
|
c.Set("product_code", productCode)
|
||||||
|
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractEnterpriseID(c *gin.Context) string {
|
||||||
|
// 优先从Header获取
|
||||||
|
if id := c.GetHeader("X-Enterprise-ID"); id != "" {
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从路径参数获取
|
||||||
|
return c.Param("enterprise_id")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 数据服务域作为主协调者
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 数据服务域的HTTP处理器
|
||||||
|
type DataServiceHandler struct {
|
||||||
|
securityService *SecurityService
|
||||||
|
userService *UserService
|
||||||
|
productService *ProductService
|
||||||
|
billingService *BillingService
|
||||||
|
auditService *AuditService
|
||||||
|
upstreamClient *UpstreamAPIClient
|
||||||
|
logger *zap.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// API端点定义
|
||||||
|
func (h *DataServiceHandler) RegisterRoutes(r *gin.RouterGroup) {
|
||||||
|
// 具体的数据产品API
|
||||||
|
r.POST("/financial-data", h.GetFinancialData)
|
||||||
|
r.POST("/credit-check", h.GetCreditCheck)
|
||||||
|
r.POST("/risk-assessment", h.GetRiskAssessment)
|
||||||
|
r.POST("/company-info", h.GetCompanyInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 具体业务处理器
|
||||||
|
func (h *DataServiceHandler) GetFinancialData(c *gin.Context) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
|
||||||
|
// 构建请求对象
|
||||||
|
req := &APIRequest{
|
||||||
|
RequestID: c.GetString("request_id"),
|
||||||
|
EnterpriseID: c.GetString("enterprise_id"),
|
||||||
|
ProductCode: "FINANCIAL_DATA",
|
||||||
|
ClientIP: c.GetString("client_ip"),
|
||||||
|
EncryptedParams: c.PostForm("data"),
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用业务逻辑
|
||||||
|
resp, err := h.ProcessAPIRequest(ctx, req)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{
|
||||||
|
"error": err.Error(),
|
||||||
|
"request_id": req.RequestID,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, resp)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔄 域间通信设计
|
||||||
|
|
||||||
|
### 1. 事件驱动架构
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 定义领域事件
|
||||||
|
type APIRequestStarted struct {
|
||||||
|
RequestID string `json:"request_id"`
|
||||||
|
EnterpriseID string `json:"enterprise_id"`
|
||||||
|
ProductCode string `json:"product_code"`
|
||||||
|
ClientIP string `json:"client_ip"`
|
||||||
|
Timestamp time.Time `json:"timestamp"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type APIRequestCompleted struct {
|
||||||
|
RequestID string `json:"request_id"`
|
||||||
|
EnterpriseID string `json:"enterprise_id"`
|
||||||
|
ProductCode string `json:"product_code"`
|
||||||
|
Success bool `json:"success"`
|
||||||
|
Cost float64 `json:"cost"`
|
||||||
|
Duration int64 `json:"duration_ms"`
|
||||||
|
Timestamp time.Time `json:"timestamp"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ChargeRequired struct {
|
||||||
|
RequestID string `json:"request_id"`
|
||||||
|
EnterpriseID string `json:"enterprise_id"`
|
||||||
|
Amount float64 `json:"amount"`
|
||||||
|
ProductCode string `json:"product_code"`
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 异步处理
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 异步计费处理
|
||||||
|
func (s *BillingService) HandleChargeRequired(event ChargeRequired) error {
|
||||||
|
// 异步处理计费,避免阻塞主流程
|
||||||
|
go func() {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if err := s.ProcessCharge(ctx, event); err != nil {
|
||||||
|
s.logger.Error("async charge failed",
|
||||||
|
zap.Error(err),
|
||||||
|
zap.String("request_id", event.RequestID))
|
||||||
|
|
||||||
|
// 发送补偿事件
|
||||||
|
s.eventBus.Publish(ChargeFailed{
|
||||||
|
RequestID: event.RequestID,
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📊 性能和可扩展性考虑
|
||||||
|
|
||||||
|
### 1. 缓存策略
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 多级缓存策略
|
||||||
|
type CacheStrategy struct {
|
||||||
|
// L1: 本地缓存 (企业信息、产品配置)
|
||||||
|
localCache *cache.Cache
|
||||||
|
|
||||||
|
// L2: Redis缓存 (密钥、白名单)
|
||||||
|
redisCache *redis.Client
|
||||||
|
|
||||||
|
// L3: 数据库
|
||||||
|
db *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cs *CacheStrategy) GetEnterpriseSecret(enterpriseID string) (string, error) {
|
||||||
|
// L1缓存查找
|
||||||
|
if secret, found := cs.localCache.Get("secret:" + enterpriseID); found {
|
||||||
|
return secret.(string), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// L2缓存查找
|
||||||
|
if secret, err := cs.redisCache.Get(context.Background(), "secret:"+enterpriseID).Result(); err == nil {
|
||||||
|
cs.localCache.Set("secret:"+enterpriseID, secret, 5*time.Minute)
|
||||||
|
return secret, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// L3数据库查找
|
||||||
|
var enterprise Enterprise
|
||||||
|
if err := cs.db.Where("id = ?", enterpriseID).First(&enterprise).Error; err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 写入缓存
|
||||||
|
cs.redisCache.Set(context.Background(), "secret:"+enterpriseID, enterprise.SecretKey, time.Hour)
|
||||||
|
cs.localCache.Set("secret:"+enterpriseID, enterprise.SecretKey, 5*time.Minute)
|
||||||
|
|
||||||
|
return enterprise.SecretKey, nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 熔断器模式
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 上游API调用熔断器
|
||||||
|
func (s *DataService) callUpstreamAPI(ctx context.Context, params map[string]interface{}) (*UpstreamResponse, error) {
|
||||||
|
return s.circuitBreaker.Execute(func() (interface{}, error) {
|
||||||
|
client := &http.Client{Timeout: 30 * time.Second}
|
||||||
|
|
||||||
|
// 构建请求
|
||||||
|
reqBody, _ := json.Marshal(params)
|
||||||
|
req, _ := http.NewRequestWithContext(ctx, "POST", s.upstreamURL, bytes.NewBuffer(reqBody))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("Authorization", "Bearer "+s.upstreamToken)
|
||||||
|
|
||||||
|
// 发送请求
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
// 解析响应
|
||||||
|
var upstreamResp UpstreamResponse
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&upstreamResp); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &upstreamResp, nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎯 总结
|
||||||
|
|
||||||
|
### 涉及的域数量:**7 个域**
|
||||||
|
|
||||||
|
1. 网关域 - 统一入口
|
||||||
|
2. 安全域 - 认证加密
|
||||||
|
3. 用户域 - 企业验证
|
||||||
|
4. 产品域 - 权限控制
|
||||||
|
5. 数据服务域 - 核心业务
|
||||||
|
6. 计费域 - 扣费计算
|
||||||
|
7. 审计域 - 请求记录
|
||||||
|
|
||||||
|
### 路由入口设计:
|
||||||
|
|
||||||
|
- **主入口**: 网关域 (`/api/v1/data/`)
|
||||||
|
- **业务协调**: 数据服务域作为主要协调者
|
||||||
|
- **域间通信**: 通过 gRPC 调用和事件总线
|
||||||
|
|
||||||
|
### 设计优势:
|
||||||
|
|
||||||
|
1. **清晰的职责分离** - 每个域专注自己的业务
|
||||||
|
2. **高可扩展性** - 可以独立扩展任何一个域
|
||||||
|
3. **易于测试** - 每个域可以独立测试
|
||||||
|
4. **容错性强** - 单个域故障不影响整体
|
||||||
|
5. **性能优化** - 多级缓存和异步处理
|
||||||
|
|
||||||
|
这种设计既保证了业务逻辑的清晰性,又确保了系统的高性能和高可用性。
|
||||||
398
Domain域详解.md
Normal file
398
Domain域详解.md
Normal file
@@ -0,0 +1,398 @@
|
|||||||
|
# Domain 域概念详解
|
||||||
|
|
||||||
|
## 🤔 什么是 Domain(域)?
|
||||||
|
|
||||||
|
**Domain(域)** 就像现实世界中的**部门**或**业务范围**。
|
||||||
|
|
||||||
|
### 📚 现实世界的类比
|
||||||
|
|
||||||
|
想象一个大公司:
|
||||||
|
|
||||||
|
```
|
||||||
|
阿里巴巴集团
|
||||||
|
├── 电商域 (淘宝、天猫)
|
||||||
|
├── 支付域 (支付宝)
|
||||||
|
├── 云计算域 (阿里云)
|
||||||
|
├── 物流域 (菜鸟)
|
||||||
|
└── 金融域 (蚂蚁金服)
|
||||||
|
```
|
||||||
|
|
||||||
|
每个域都有:
|
||||||
|
|
||||||
|
- **专门的团队** - 不同的开发团队
|
||||||
|
- **独立的业务** - 各自负责不同的业务功能
|
||||||
|
- **清晰的边界** - 知道自己管什么,不管什么
|
||||||
|
- **独立运作** - 可以独立决策和发展
|
||||||
|
|
||||||
|
## 🏗️ 在软件架构中的应用
|
||||||
|
|
||||||
|
### 传统方式 vs 域驱动方式
|
||||||
|
|
||||||
|
#### ❌ 传统方式(技术驱动)
|
||||||
|
|
||||||
|
```
|
||||||
|
项目结构:
|
||||||
|
├── controllers/ # 所有控制器
|
||||||
|
├── services/ # 所有服务
|
||||||
|
├── models/ # 所有数据模型
|
||||||
|
└── utils/ # 工具类
|
||||||
|
|
||||||
|
问题:
|
||||||
|
- 用户相关、产品相关、支付相关的代码混在一起
|
||||||
|
- 不同业务逻辑耦合
|
||||||
|
- 团队协作困难
|
||||||
|
- 修改一个功能可能影响其他功能
|
||||||
|
```
|
||||||
|
|
||||||
|
#### ✅ 域驱动方式(业务驱动)
|
||||||
|
|
||||||
|
```
|
||||||
|
项目结构:
|
||||||
|
├── user-domain/ # 用户域
|
||||||
|
│ ├── user/ # 用户管理
|
||||||
|
│ ├── auth/ # 认证授权
|
||||||
|
│ └── profile/ # 用户资料
|
||||||
|
├── product-domain/ # 产品域
|
||||||
|
│ ├── catalog/ # 产品目录
|
||||||
|
│ ├── inventory/ # 库存管理
|
||||||
|
│ └── pricing/ # 价格管理
|
||||||
|
├── payment-domain/ # 支付域
|
||||||
|
│ ├── wallet/ # 钱包
|
||||||
|
│ ├── order/ # 订单
|
||||||
|
│ └── billing/ # 计费
|
||||||
|
└── notification-domain/ # 通知域
|
||||||
|
├── email/ # 邮件通知
|
||||||
|
├── sms/ # 短信通知
|
||||||
|
└── push/ # 推送通知
|
||||||
|
|
||||||
|
优势:
|
||||||
|
- 按业务功能组织代码
|
||||||
|
- 团队可以独立开发不同的域
|
||||||
|
- 修改用户功能不会影响支付功能
|
||||||
|
- 新人容易理解业务边界
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎯 你的项目中的 Domain 重构
|
||||||
|
|
||||||
|
### 当前问题
|
||||||
|
|
||||||
|
你的项目中有这些编码式命名:
|
||||||
|
|
||||||
|
```
|
||||||
|
IVYZ - 不知道是什么业务
|
||||||
|
FLXG - 不知道是什么业务
|
||||||
|
QYGL - 不知道是什么业务
|
||||||
|
YYSY - 不知道是什么业务
|
||||||
|
JRZQ - 不知道是什么业务
|
||||||
|
```
|
||||||
|
|
||||||
|
### 重构后的 Domain 设计
|
||||||
|
|
||||||
|
```
|
||||||
|
用户域 (User Domain)
|
||||||
|
├── 用户注册登录
|
||||||
|
├── 用户信息管理
|
||||||
|
├── 企业认证
|
||||||
|
└── 权限管理
|
||||||
|
|
||||||
|
金融域 (Financial Domain)
|
||||||
|
├── 钱包管理
|
||||||
|
├── 支付处理
|
||||||
|
├── 账单生成
|
||||||
|
└── 财务报表
|
||||||
|
|
||||||
|
数据服务域 (Data Service Domain)
|
||||||
|
├── 风险评估服务 (原 FLXG)
|
||||||
|
├── 征信查询服务 (原 JRZQ)
|
||||||
|
├── 企业信息服务 (原 QYGL)
|
||||||
|
├── 数据查询服务 (原 IVYZ)
|
||||||
|
└── 应用系统服务 (原 YYSY)
|
||||||
|
|
||||||
|
产品域 (Product Domain)
|
||||||
|
├── 产品目录管理
|
||||||
|
├── 产品订阅
|
||||||
|
├── 访问控制
|
||||||
|
└── 白名单管理
|
||||||
|
|
||||||
|
平台域 (Platform Domain)
|
||||||
|
├── 管理后台
|
||||||
|
├── 系统监控
|
||||||
|
├── 日志管理
|
||||||
|
└── 配置管理
|
||||||
|
```
|
||||||
|
|
||||||
|
## 💻 代码层面的 Domain 实现
|
||||||
|
|
||||||
|
### 1. Domain 层的职责
|
||||||
|
|
||||||
|
```go
|
||||||
|
// domain/user/entity/user.go
|
||||||
|
package entity
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// User 用户实体
|
||||||
|
type User struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
Phone string `json:"phone"`
|
||||||
|
Status UserStatus `json:"status"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserStatus int
|
||||||
|
|
||||||
|
const (
|
||||||
|
UserStatusActive UserStatus = 1
|
||||||
|
UserStatusInactive UserStatus = 2
|
||||||
|
UserStatusBanned UserStatus = 3
|
||||||
|
)
|
||||||
|
|
||||||
|
// 业务规则:用户名必须是3-20个字符
|
||||||
|
func (u *User) ValidateUsername() error {
|
||||||
|
if len(u.Username) < 3 || len(u.Username) > 20 {
|
||||||
|
return errors.New("用户名必须是3-20个字符")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 业务规则:检查用户是否可以登录
|
||||||
|
func (u *User) CanLogin() bool {
|
||||||
|
return u.Status == UserStatusActive
|
||||||
|
}
|
||||||
|
|
||||||
|
// 业务规则:激活用户
|
||||||
|
func (u *User) Activate() error {
|
||||||
|
if u.Status == UserStatusBanned {
|
||||||
|
return errors.New("被封禁的用户不能激活")
|
||||||
|
}
|
||||||
|
u.Status = UserStatusActive
|
||||||
|
u.UpdatedAt = time.Now()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Domain Service (领域服务)
|
||||||
|
|
||||||
|
```go
|
||||||
|
// domain/user/service/user_service.go
|
||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"your-project/domain/user/entity"
|
||||||
|
"your-project/domain/user/repository"
|
||||||
|
)
|
||||||
|
|
||||||
|
// UserDomainService 用户领域服务
|
||||||
|
type UserDomainService struct {
|
||||||
|
userRepo repository.UserRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUserDomainService(userRepo repository.UserRepository) *UserDomainService {
|
||||||
|
return &UserDomainService{
|
||||||
|
userRepo: userRepo,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 业务规则:检查用户名是否唯一
|
||||||
|
func (s *UserDomainService) IsUsernameUnique(ctx context.Context, username string) (bool, error) {
|
||||||
|
existingUser, err := s.userRepo.FindByUsername(ctx, username)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return existingUser == nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 业务规则:用户注册
|
||||||
|
func (s *UserDomainService) RegisterUser(ctx context.Context, username, email, phone string) (*entity.User, error) {
|
||||||
|
// 1. 检查用户名唯一性
|
||||||
|
unique, err := s.IsUsernameUnique(ctx, username)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !unique {
|
||||||
|
return nil, errors.New("用户名已存在")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 创建用户实体
|
||||||
|
user := &entity.User{
|
||||||
|
Username: username,
|
||||||
|
Email: email,
|
||||||
|
Phone: phone,
|
||||||
|
Status: entity.UserStatusActive,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 验证业务规则
|
||||||
|
if err := user.ValidateUsername(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 保存用户
|
||||||
|
return s.userRepo.Save(ctx, user)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Repository 接口 (仓储模式)
|
||||||
|
|
||||||
|
```go
|
||||||
|
// domain/user/repository/user_repository.go
|
||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"your-project/domain/user/entity"
|
||||||
|
)
|
||||||
|
|
||||||
|
// UserRepository 用户仓储接口
|
||||||
|
type UserRepository interface {
|
||||||
|
Save(ctx context.Context, user *entity.User) (*entity.User, error)
|
||||||
|
FindByID(ctx context.Context, id int64) (*entity.User, error)
|
||||||
|
FindByUsername(ctx context.Context, username string) (*entity.User, error)
|
||||||
|
FindByEmail(ctx context.Context, email string) (*entity.User, error)
|
||||||
|
Update(ctx context.Context, user *entity.User) error
|
||||||
|
Delete(ctx context.Context, id int64) error
|
||||||
|
List(ctx context.Context, offset, limit int) ([]*entity.User, error)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 应用层 (Application Layer)
|
||||||
|
|
||||||
|
```go
|
||||||
|
// application/user/service/user_app_service.go
|
||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
userDomain "your-project/domain/user/service"
|
||||||
|
"your-project/application/user/dto"
|
||||||
|
)
|
||||||
|
|
||||||
|
// UserAppService 用户应用服务
|
||||||
|
type UserAppService struct {
|
||||||
|
userDomainService *userDomain.UserDomainService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUserAppService(userDomainService *userDomain.UserDomainService) *UserAppService {
|
||||||
|
return &UserAppService{
|
||||||
|
userDomainService: userDomainService,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用户注册用例
|
||||||
|
func (s *UserAppService) RegisterUser(ctx context.Context, req *dto.RegisterUserRequest) (*dto.UserResponse, error) {
|
||||||
|
// 1. 调用领域服务
|
||||||
|
user, err := s.userDomainService.RegisterUser(ctx, req.Username, req.Email, req.Phone)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 转换为 DTO
|
||||||
|
return &dto.UserResponse{
|
||||||
|
ID: user.ID,
|
||||||
|
Username: user.Username,
|
||||||
|
Email: user.Email,
|
||||||
|
Phone: user.Phone,
|
||||||
|
Status: string(user.Status),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔄 域与域之间的交互
|
||||||
|
|
||||||
|
### 事件驱动交互
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 用户注册成功后,通知其他域
|
||||||
|
type UserRegisteredEvent struct {
|
||||||
|
UserID int64 `json:"user_id"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 金融域监听用户注册事件,自动创建钱包
|
||||||
|
func (h *WalletEventHandler) HandleUserRegistered(event UserRegisteredEvent) error {
|
||||||
|
// 为新用户创建钱包
|
||||||
|
wallet := &entity.Wallet{
|
||||||
|
UserID: event.UserID,
|
||||||
|
Balance: 0.0,
|
||||||
|
Currency: "CNY",
|
||||||
|
}
|
||||||
|
return h.walletRepo.Save(context.Background(), wallet)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📊 Domain 的好处
|
||||||
|
|
||||||
|
### 1. **团队协作**
|
||||||
|
|
||||||
|
```
|
||||||
|
用户域团队:专注用户相关功能
|
||||||
|
├── 张三:负责用户注册登录
|
||||||
|
├── 李四:负责用户资料管理
|
||||||
|
└── 王五:负责企业认证
|
||||||
|
|
||||||
|
支付域团队:专注支付相关功能
|
||||||
|
├── 赵六:负责钱包功能
|
||||||
|
├── 孙七:负责支付流程
|
||||||
|
└── 周八:负责账单生成
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. **独立部署**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 可以独立部署不同的域
|
||||||
|
kubectl apply -f user-domain-deployment.yaml
|
||||||
|
kubectl apply -f payment-domain-deployment.yaml
|
||||||
|
kubectl apply -f product-domain-deployment.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. **技术选择自由**
|
||||||
|
|
||||||
|
```
|
||||||
|
用户域:使用 PostgreSQL (复杂查询)
|
||||||
|
支付域:使用 MySQL (事务性强)
|
||||||
|
数据域:使用 ClickHouse (分析查询)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. **测试隔离**
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 只测试用户域,不需要启动其他域
|
||||||
|
func TestUserDomain(t *testing.T) {
|
||||||
|
// 只需要用户域的依赖
|
||||||
|
userRepo := mock.NewUserRepository()
|
||||||
|
userService := service.NewUserDomainService(userRepo)
|
||||||
|
|
||||||
|
// 测试用户注册逻辑
|
||||||
|
user, err := userService.RegisterUser(ctx, "testuser", "test@example.com", "13800138000")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "testuser", user.Username)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎯 总结
|
||||||
|
|
||||||
|
**Domain(域)就是按业务功能划分的代码组织方式**:
|
||||||
|
|
||||||
|
1. **用户域** - 管理用户相关的所有功能
|
||||||
|
2. **支付域** - 管理支付相关的所有功能
|
||||||
|
3. **产品域** - 管理产品相关的所有功能
|
||||||
|
4. **数据域** - 管理数据查询相关的功能
|
||||||
|
|
||||||
|
**核心思想**:
|
||||||
|
|
||||||
|
- 按业务划分,不按技术划分
|
||||||
|
- 每个域独立开发、测试、部署
|
||||||
|
- 域之间通过事件或 API 通信
|
||||||
|
- 团队可以专注于特定业务领域
|
||||||
|
|
||||||
|
这样组织代码后,你的项目会更容易理解、维护和扩展!
|
||||||
2
aes.go
2
aes.go
@@ -96,7 +96,7 @@ func main() {
|
|||||||
|
|
||||||
var data interface{}
|
var data interface{}
|
||||||
|
|
||||||
decrypt, err := AesDecrypt("cTOkAjH96uOrIoCP4M67HnmXT/fOxEOiAgzNFbb8f+3BJTyAEyX1WXk2MDAnRz+mKl+W7HrDO8eYYEHJG1H7OY+JlXFPNGlwR+iREavdsLXcxoVlaNLfKoFW3EOX07gKuqFxwOPGfdvdIPp/5xC7o0gmsi+6evIt6vSK2Ko/YuGYDVGDzlAFY5wdhWH9/4bQzFdI234k5qds2LFJnYsZ/rNWiCfNisM0oLqv15YCe1yDAvV7OVcRcj/rjCn4K1U7xEXr4WWLh/0nTFUnbf2SHL9DpA6pQ0hXwow5v9o1S74bdzQTbloA3nYyFav26u/Q7/ElHaxrmbqQBzbLxM+QMsMTn2G6rFtYRSxlGNrIM6KTb0hZtusdDBr6ndXBLkwj9RZqJ5cxTI1a2+i6EudnjA==", key)
|
decrypt, err := AesDecrypt("gSSLA3V+MoabjTyPiCvYC6bg7TLk+ja/Zly3R8wjpK/xPC6ZK0QCwdpukGEuUVKdfOU2IU46Q6kSZHXmpF8MdXEa0NKNh85yhlFQVy0U2jJkTxojvoM+1Y/iZlVZpFrx91n7+KdtZkVWkXSZk5eJcizYmLUcaO86ad7PfvQvT4=", key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("解密错误:", err)
|
fmt.Println("解密错误:", err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -85,6 +85,9 @@ service api-api {
|
|||||||
|
|
||||||
@handler FLXG0V4B
|
@handler FLXG0V4B
|
||||||
post /FLXG0V4B (request) returns (string)
|
post /FLXG0V4B (request) returns (string)
|
||||||
|
|
||||||
|
@handler FLXG0687
|
||||||
|
post /FLXG0687 (request) returns (string)
|
||||||
}
|
}
|
||||||
|
|
||||||
@server (
|
@server (
|
||||||
@@ -157,4 +160,18 @@ service api-api {
|
|||||||
|
|
||||||
@handler JRZQ4AA8
|
@handler JRZQ4AA8
|
||||||
post /JRZQ4AA8 (request) returns (string)
|
post /JRZQ4AA8 (request) returns (string)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@server (
|
||||||
|
group: COMB
|
||||||
|
prefix: /api/v1
|
||||||
|
middleware: ApiAuthInterceptor
|
||||||
|
)
|
||||||
|
service api-api {
|
||||||
|
@handler COMB298Y
|
||||||
|
post /COMB298Y (request) returns (string)
|
||||||
|
|
||||||
|
@handler COMB86PM
|
||||||
|
post /COMB86PM (request) returns (string)
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,43 @@
|
|||||||
package common
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
)
|
||||||
|
|
||||||
|
// StructToMap 将结构体转换为 map[string]interface{}
|
||||||
|
func StructToMap(data interface{}) (map[string]interface{}, error) {
|
||||||
|
result := make(map[string]interface{})
|
||||||
|
|
||||||
|
// 使用反射获取结构体的值
|
||||||
|
v := reflect.ValueOf(data)
|
||||||
|
if v.Kind() == reflect.Ptr {
|
||||||
|
v = v.Elem()
|
||||||
|
}
|
||||||
|
if v.Kind() != reflect.Struct {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 遍历结构体字段
|
||||||
|
t := v.Type()
|
||||||
|
for i := 0; i < v.NumField(); i++ {
|
||||||
|
field := t.Field(i)
|
||||||
|
value := v.Field(i)
|
||||||
|
|
||||||
|
// 使用结构体字段名作为 key
|
||||||
|
key := field.Name
|
||||||
|
|
||||||
|
// 处理字段值
|
||||||
|
if value.IsValid() && !value.IsZero() {
|
||||||
|
if value.Kind() == reflect.Ptr {
|
||||||
|
value = value.Elem()
|
||||||
|
}
|
||||||
|
result[key] = value.Interface()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
func MapStructToAPIRequest(encryptedFields map[string]interface{}, fieldMapping map[string]string, wrapField string) map[string]interface{} {
|
func MapStructToAPIRequest(encryptedFields map[string]interface{}, fieldMapping map[string]string, wrapField string) map[string]interface{} {
|
||||||
apiRequest := make(map[string]interface{})
|
apiRequest := make(map[string]interface{})
|
||||||
|
|
||||||
|
|||||||
31
apps/api/internal/handler/COMB/comb298yhandler.go
Normal file
31
apps/api/internal/handler/COMB/comb298yhandler.go
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
package COMB
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"tianyuan-api/apps/api/internal/logic/COMB"
|
||||||
|
"tianyuan-api/apps/api/internal/svc"
|
||||||
|
"tianyuan-api/apps/api/internal/types"
|
||||||
|
"tianyuan-api/pkg/errs"
|
||||||
|
"tianyuan-api/pkg/response"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
|
)
|
||||||
|
|
||||||
|
func COMB298YHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req types.Request
|
||||||
|
if err := httpx.Parse(r, &req); err != nil {
|
||||||
|
response.Fail(r.Context(), w, errs.ErrParamValidation, nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
l := COMB.NewCOMB298YLogic(r.Context(), svcCtx)
|
||||||
|
resp, err := l.COMB298Y(&req)
|
||||||
|
if err != nil {
|
||||||
|
response.Fail(r.Context(), w, err, resp)
|
||||||
|
} else {
|
||||||
|
response.Success(r.Context(), w, resp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
31
apps/api/internal/handler/COMB/comb86pmhandler.go
Normal file
31
apps/api/internal/handler/COMB/comb86pmhandler.go
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
package COMB
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"tianyuan-api/apps/api/internal/logic/COMB"
|
||||||
|
"tianyuan-api/apps/api/internal/svc"
|
||||||
|
"tianyuan-api/apps/api/internal/types"
|
||||||
|
"tianyuan-api/pkg/errs"
|
||||||
|
"tianyuan-api/pkg/response"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
|
)
|
||||||
|
|
||||||
|
func COMB86PMHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req types.Request
|
||||||
|
if err := httpx.Parse(r, &req); err != nil {
|
||||||
|
response.Fail(r.Context(), w, errs.ErrParamValidation, nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
l := COMB.NewCOMB86PMLogic(r.Context(), svcCtx)
|
||||||
|
resp, err := l.COMB86PM(&req)
|
||||||
|
if err != nil {
|
||||||
|
response.Fail(r.Context(), w, err, resp)
|
||||||
|
} else {
|
||||||
|
response.Success(r.Context(), w, resp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
31
apps/api/internal/handler/FLXG/flxg0687handler.go
Normal file
31
apps/api/internal/handler/FLXG/flxg0687handler.go
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
package FLXG
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"tianyuan-api/pkg/errs"
|
||||||
|
"tianyuan-api/pkg/response"
|
||||||
|
|
||||||
|
"tianyuan-api/apps/api/internal/logic/FLXG"
|
||||||
|
"tianyuan-api/apps/api/internal/svc"
|
||||||
|
"tianyuan-api/apps/api/internal/types"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
|
)
|
||||||
|
|
||||||
|
func FLXG0687Handler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req types.Request
|
||||||
|
if err := httpx.Parse(r, &req); err != nil {
|
||||||
|
response.Fail(r.Context(), w, errs.ErrParamValidation, nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
l := FLXG.NewFLXG0687Logic(r.Context(), svcCtx)
|
||||||
|
resp, err := l.FLXG0687(&req)
|
||||||
|
if err != nil {
|
||||||
|
response.Fail(r.Context(), w, err, resp)
|
||||||
|
} else {
|
||||||
|
response.Success(r.Context(), w, resp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
// Code generated by goctl. DO NOT EDIT.
|
// Code generated by goctl. DO NOT EDIT.
|
||||||
// goctl 1.7.3
|
// goctl 1.8.4
|
||||||
|
|
||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
COMB "tianyuan-api/apps/api/internal/handler/COMB"
|
||||||
FLXG "tianyuan-api/apps/api/internal/handler/FLXG"
|
FLXG "tianyuan-api/apps/api/internal/handler/FLXG"
|
||||||
IVYZ "tianyuan-api/apps/api/internal/handler/IVYZ"
|
IVYZ "tianyuan-api/apps/api/internal/handler/IVYZ"
|
||||||
JRZQ "tianyuan-api/apps/api/internal/handler/JRZQ"
|
JRZQ "tianyuan-api/apps/api/internal/handler/JRZQ"
|
||||||
@@ -21,6 +22,30 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
|
|||||||
rest.WithMiddlewares(
|
rest.WithMiddlewares(
|
||||||
[]rest.Middleware{serverCtx.ApiAuthInterceptor},
|
[]rest.Middleware{serverCtx.ApiAuthInterceptor},
|
||||||
[]rest.Route{
|
[]rest.Route{
|
||||||
|
{
|
||||||
|
Method: http.MethodPost,
|
||||||
|
Path: "/COMB298Y",
|
||||||
|
Handler: COMB.COMB298YHandler(serverCtx),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Method: http.MethodPost,
|
||||||
|
Path: "/COMB86PM",
|
||||||
|
Handler: COMB.COMB86PMHandler(serverCtx),
|
||||||
|
},
|
||||||
|
}...,
|
||||||
|
),
|
||||||
|
rest.WithPrefix("/api/v1"),
|
||||||
|
)
|
||||||
|
|
||||||
|
server.AddRoutes(
|
||||||
|
rest.WithMiddlewares(
|
||||||
|
[]rest.Middleware{serverCtx.ApiAuthInterceptor},
|
||||||
|
[]rest.Route{
|
||||||
|
{
|
||||||
|
Method: http.MethodPost,
|
||||||
|
Path: "/FLXG0687",
|
||||||
|
Handler: FLXG.FLXG0687Handler(serverCtx),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Method: http.MethodPost,
|
Method: http.MethodPost,
|
||||||
Path: "/FLXG0V3B",
|
Path: "/FLXG0V3B",
|
||||||
|
|||||||
311
apps/api/internal/logic/COMB/comb298ylogic.go
Normal file
311
apps/api/internal/logic/COMB/comb298ylogic.go
Normal file
@@ -0,0 +1,311 @@
|
|||||||
|
package COMB
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"tianyuan-api/apps/api/internal/common"
|
||||||
|
"tianyuan-api/apps/api/internal/svc"
|
||||||
|
"tianyuan-api/apps/api/internal/types"
|
||||||
|
"tianyuan-api/apps/api/internal/validator"
|
||||||
|
"tianyuan-api/apps/api/internal/westmodel"
|
||||||
|
"tianyuan-api/pkg/crypto"
|
||||||
|
"tianyuan-api/pkg/errs"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/core/logx"
|
||||||
|
)
|
||||||
|
|
||||||
|
type COMB298YLogic struct {
|
||||||
|
logx.Logger
|
||||||
|
ctx context.Context
|
||||||
|
svcCtx *svc.ServiceContext
|
||||||
|
}
|
||||||
|
|
||||||
|
// WestAPIRequest 定义西部API请求结构
|
||||||
|
type APIRequest struct {
|
||||||
|
SourceId string
|
||||||
|
ServiceId string
|
||||||
|
Request map[string]interface{}
|
||||||
|
Mapping map[string]string
|
||||||
|
Wrap string
|
||||||
|
Service string
|
||||||
|
}
|
||||||
|
|
||||||
|
// WestAPIResponse 定义西部API响应结构
|
||||||
|
type APIResponse struct {
|
||||||
|
ServiceId string
|
||||||
|
Resp json.RawMessage
|
||||||
|
Success bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将所有响应组装成JSON
|
||||||
|
type ResponseData struct {
|
||||||
|
Responses []struct {
|
||||||
|
ServiceId string `json:"api_code"`
|
||||||
|
Data json.RawMessage `json:"data"`
|
||||||
|
Success bool `json:"success"`
|
||||||
|
} `json:"responses"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCOMB298YLogic(ctx context.Context, svcCtx *svc.ServiceContext) *COMB298YLogic {
|
||||||
|
return &COMB298YLogic{
|
||||||
|
Logger: logx.WithContext(ctx),
|
||||||
|
ctx: ctx,
|
||||||
|
svcCtx: svcCtx,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *COMB298YLogic) COMB298Y(req *types.Request) (resp string, err *errs.AppError) {
|
||||||
|
var status string
|
||||||
|
var charges bool
|
||||||
|
var remark = ""
|
||||||
|
secretKey, ok := l.ctx.Value("secretKey").(string)
|
||||||
|
if !ok {
|
||||||
|
return "", errs.ErrSystem
|
||||||
|
}
|
||||||
|
transactionID, ok := l.ctx.Value("transactionID").(string)
|
||||||
|
if !ok {
|
||||||
|
return "", errs.ErrSystem
|
||||||
|
}
|
||||||
|
userId, userIdOk := l.ctx.Value("userId").(int64)
|
||||||
|
if !userIdOk {
|
||||||
|
return "", errs.ErrSystem
|
||||||
|
}
|
||||||
|
productCode, productCodeOk := l.ctx.Value("productCode").(string)
|
||||||
|
if !productCodeOk || productCode == "" {
|
||||||
|
return "", errs.ErrSystem
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if err != nil {
|
||||||
|
status = "failed"
|
||||||
|
charges = false
|
||||||
|
} else {
|
||||||
|
status = "success"
|
||||||
|
charges = true
|
||||||
|
}
|
||||||
|
sendApiRequestMessageErr := l.svcCtx.ApiRequestMqsService.SendApiRequestMessage(l.ctx, transactionID, userId, productCode, status, charges, remark)
|
||||||
|
if sendApiRequestMessageErr != nil {
|
||||||
|
logx.Errorf("发送 API 请求消息失败: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
// 1、解密
|
||||||
|
key, decodeErr := hex.DecodeString(secretKey)
|
||||||
|
if decodeErr != nil {
|
||||||
|
return "", errs.ErrSystem
|
||||||
|
}
|
||||||
|
decryptData, aesDecryptErr := crypto.AesDecrypt(req.Data, key)
|
||||||
|
if aesDecryptErr != nil || len(decryptData) == 0 {
|
||||||
|
return "", errs.ErrParamDecryption
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2、校验
|
||||||
|
var data validator.COMB298YRequest
|
||||||
|
if validatorErr := validator.ValidateAndParse(decryptData, &data); validatorErr != nil {
|
||||||
|
return "", errs.ErrParamValidation
|
||||||
|
}
|
||||||
|
|
||||||
|
// 准备并发请求
|
||||||
|
apiRequests := []APIRequest{
|
||||||
|
{SourceId: "G16BJ02", ServiceId: "YYSY09CD", Mapping: westmodel.YYSY09CDFieldMapping, Wrap: "data", Service: "west"}, // 运营商三要素简版
|
||||||
|
{SourceId: "G27BJ05", ServiceId: "JRZQ0A03", Mapping: westmodel.JRZQ0A03FieldMapping, Wrap: "data", Service: "west"}, // 借贷意向
|
||||||
|
{SourceId: "G28BJ05", ServiceId: "JRZQ8203", Mapping: westmodel.JRZQ8203FieldMapping, Wrap: "data", Service: "west"}, // 借贷行为
|
||||||
|
{SourceId: "G26BJ05", ServiceId: "FLXG3D56", Mapping: westmodel.FLXG3D56FieldMapping, Wrap: "data", Service: "west"}, // 特殊名单
|
||||||
|
{SourceId: "G22SC01", ServiceId: "FLXGCA3D", Mapping: westmodel.FLXGCA3DFieldMapping, Wrap: "data", Service: "west"}, // 综合涉诉
|
||||||
|
{SourceId: "G29BJ05", ServiceId: "JRZQ4AA8", Mapping: westmodel.JRZQ4AA8FieldMapping, Wrap: "data", Service: "west"}, // 偿贷压力
|
||||||
|
{SourceId: "G30BJ05", ServiceId: "FLXGC9D1", Mapping: westmodel.FLXGC9D1FieldMapping, Wrap: "data", Service: "west"}, // 黑灰产等级
|
||||||
|
{SourceId: "G32BJ05", ServiceId: "FLXG162A", Mapping: westmodel.FLXG162AFieldMapping, Wrap: "data", Service: "west"}, // 团伙欺诈
|
||||||
|
{SourceId: "RIS031", ServiceId: "FLXG0687", Mapping: westmodel.FLXG0687FieldMapping, Wrap: "", Service: "yushan"}, // 反赌反诈
|
||||||
|
{SourceId: "G09XM02", ServiceId: "IVYZ5733", Mapping: westmodel.IVYZ5733FieldMapping, Wrap: "data", Service: "west"},
|
||||||
|
{SourceId: "G11BJ06", ServiceId: "IVYZ9A2B", Mapping: westmodel.IVYZ9A2BFieldMapping, Wrap: "data", Service: "west"},
|
||||||
|
{SourceId: "G03HZ01", ServiceId: "FLXG54F5", Mapping: westmodel.FLXG54F5FieldMapping, Wrap: "data", Service: "west"},
|
||||||
|
{SourceId: "G05HZ01", ServiceId: "QYGLB4C0", Mapping: westmodel.QYGLB4C0FieldMapping, Wrap: "", Service: "west"},
|
||||||
|
}
|
||||||
|
|
||||||
|
// 为每个请求构建对应的请求参数
|
||||||
|
for i := range apiRequests {
|
||||||
|
if apiRequests[i].Service == "west" {
|
||||||
|
// 西部服务:先加密后mapping
|
||||||
|
westConfig := l.svcCtx.Config.WestConfig
|
||||||
|
if apiRequests[i].SourceId == "G05HZ01" {
|
||||||
|
// G05HZ01 不需要加密,直接使用原始数据
|
||||||
|
dataMap, err := common.StructToMap(data)
|
||||||
|
if err != nil {
|
||||||
|
logx.Errorf("结构体转map失败:%v", err)
|
||||||
|
return "", errs.ErrSystem
|
||||||
|
}
|
||||||
|
logx.Infof("G05HZ01 原始数据: %+v", data)
|
||||||
|
logx.Infof("G05HZ01 转换后数据: %+v", dataMap)
|
||||||
|
apiRequests[i].Request = common.MapStructToAPIRequest(dataMap, apiRequests[i].Mapping, apiRequests[i].Wrap)
|
||||||
|
logx.Infof("G05HZ01 最终请求数据: %+v", apiRequests[i].Request)
|
||||||
|
} else {
|
||||||
|
// 其他西部服务需要加密
|
||||||
|
encryptedRequest, encryptErr := common.EncryptStructFields(data, westConfig.Key)
|
||||||
|
if encryptErr != nil {
|
||||||
|
logx.Errorf("西部加密错误:%v", encryptErr)
|
||||||
|
return "", errs.ErrSystem
|
||||||
|
}
|
||||||
|
apiRequests[i].Request = common.MapStructToAPIRequest(encryptedRequest, apiRequests[i].Mapping, apiRequests[i].Wrap)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 如果是 RIS031,添加 type: 3
|
||||||
|
if apiRequests[i].SourceId == "RIS031" {
|
||||||
|
apiRequests[i].Request = map[string]interface{}{
|
||||||
|
"keyWord": data.IDCard,
|
||||||
|
"type": 3,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logx.Infof("sourceId:%s,请求参数:%v", apiRequests[i].SourceId, apiRequests[i].Request)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建响应通道
|
||||||
|
responseChan := make(chan APIResponse, len(apiRequests))
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
|
// 并发处理请求
|
||||||
|
for _, apiReq := range apiRequests {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(req APIRequest) {
|
||||||
|
defer wg.Done()
|
||||||
|
success := true
|
||||||
|
var westResp string
|
||||||
|
var callAPIErr *errs.AppError
|
||||||
|
|
||||||
|
// 根据服务类型选择不同的调用方式
|
||||||
|
switch req.Service {
|
||||||
|
case "west":
|
||||||
|
// 4、发送请求到西部
|
||||||
|
logx.Infof("交易号:%s", transactionID)
|
||||||
|
var respData []byte
|
||||||
|
if req.SourceId == "G05HZ01" {
|
||||||
|
respData, callAPIErr = l.svcCtx.WestDexService.CallAPISecond(req.SourceId, req.Request, l.svcCtx.Config.WestConfig.SecretSecondId)
|
||||||
|
} else {
|
||||||
|
respData, callAPIErr = l.svcCtx.WestDexService.CallAPI(req.SourceId, req.Request, l.svcCtx.Config.WestConfig.SecretId)
|
||||||
|
}
|
||||||
|
if callAPIErr != nil {
|
||||||
|
if callAPIErr.Code == errs.ErrDataSource.Code {
|
||||||
|
// 数据源错误(如查询无结果)是业务正常情况,记录为 info
|
||||||
|
logx.Infof("西部请求业务状态:sourceId:%s, resp:%v", req.SourceId, respData)
|
||||||
|
var jsonCheck interface{}
|
||||||
|
if json.Unmarshal(respData, &jsonCheck) != nil {
|
||||||
|
westResp = "{}"
|
||||||
|
} else {
|
||||||
|
westResp = string(respData)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 其他业务错误
|
||||||
|
logx.Errorf("西部请求业务错误:sourceId:%s,err:%v, resp:%v", req.SourceId, callAPIErr, respData)
|
||||||
|
westResp = "{}"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 确保返回的是有效的 JSON
|
||||||
|
if len(respData) == 0 {
|
||||||
|
westResp = "{}"
|
||||||
|
} else {
|
||||||
|
// 验证 JSON 是否有效
|
||||||
|
var jsonCheck interface{}
|
||||||
|
if json.Unmarshal(respData, &jsonCheck) != nil {
|
||||||
|
westResp = "{}"
|
||||||
|
} else {
|
||||||
|
westResp = string(respData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "yushan":
|
||||||
|
respData, err := l.svcCtx.YushanService.Request(req.SourceId, req.Request)
|
||||||
|
if err != nil {
|
||||||
|
logx.Errorf("羽山请求失败:sourceId:%s,err:%v", req.SourceId, err)
|
||||||
|
if appErr, ok := err.(*errs.AppError); ok {
|
||||||
|
callAPIErr = appErr
|
||||||
|
} else {
|
||||||
|
callAPIErr = errs.ErrSystem
|
||||||
|
}
|
||||||
|
westResp = "{}" // 发生错误时返回空对象
|
||||||
|
} else {
|
||||||
|
// 确保返回的是有效的 JSON
|
||||||
|
if len(respData) == 0 {
|
||||||
|
westResp = "{}"
|
||||||
|
} else {
|
||||||
|
// 验证 JSON 是否有效
|
||||||
|
var jsonCheck interface{}
|
||||||
|
if json.Unmarshal(respData, &jsonCheck) != nil {
|
||||||
|
westResp = "{}"
|
||||||
|
} else {
|
||||||
|
westResp = string(respData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
success = false
|
||||||
|
westResp = "{}"
|
||||||
|
logx.Errorf("未知的服务类型:%s", req.Service)
|
||||||
|
}
|
||||||
|
|
||||||
|
if callAPIErr != nil {
|
||||||
|
success = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保响应是有效的 JSON
|
||||||
|
var jsonResp json.RawMessage
|
||||||
|
if err := json.Unmarshal([]byte(westResp), &jsonResp); err != nil {
|
||||||
|
jsonResp = json.RawMessage("{}")
|
||||||
|
}
|
||||||
|
|
||||||
|
responseChan <- APIResponse{
|
||||||
|
ServiceId: req.ServiceId,
|
||||||
|
Resp: jsonResp,
|
||||||
|
Success: success,
|
||||||
|
}
|
||||||
|
}(apiReq)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 等待所有请求完成
|
||||||
|
go func() {
|
||||||
|
wg.Wait()
|
||||||
|
close(responseChan)
|
||||||
|
}()
|
||||||
|
|
||||||
|
// 处理响应
|
||||||
|
var responses []APIResponse
|
||||||
|
for resp := range responseChan {
|
||||||
|
responses = append(responses, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
responseData := ResponseData{
|
||||||
|
Responses: make([]struct {
|
||||||
|
ServiceId string `json:"api_code"`
|
||||||
|
Data json.RawMessage `json:"data"`
|
||||||
|
Success bool `json:"success"`
|
||||||
|
}, len(responses)),
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, resp := range responses {
|
||||||
|
responseData.Responses[i] = struct {
|
||||||
|
ServiceId string `json:"api_code"`
|
||||||
|
Data json.RawMessage `json:"data"`
|
||||||
|
Success bool `json:"success"`
|
||||||
|
}{
|
||||||
|
ServiceId: resp.ServiceId,
|
||||||
|
Data: resp.Resp,
|
||||||
|
Success: resp.Success,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将响应数据转换为JSON
|
||||||
|
jsonData, marshalErr := json.Marshal(responseData)
|
||||||
|
if marshalErr != nil {
|
||||||
|
logx.Errorf("JSON编码错误:%v", marshalErr)
|
||||||
|
return "", errs.ErrSystem
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加密JSON数据
|
||||||
|
encryptData, aesEncrypt := crypto.AesEncrypt(jsonData, key)
|
||||||
|
if aesEncrypt != nil {
|
||||||
|
return "", errs.ErrSystem
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(encryptData), nil
|
||||||
|
}
|
||||||
295
apps/api/internal/logic/COMB/comb86pmlogic.go
Normal file
295
apps/api/internal/logic/COMB/comb86pmlogic.go
Normal file
@@ -0,0 +1,295 @@
|
|||||||
|
package COMB
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"tianyuan-api/apps/api/internal/common"
|
||||||
|
"tianyuan-api/apps/api/internal/svc"
|
||||||
|
"tianyuan-api/apps/api/internal/types"
|
||||||
|
"tianyuan-api/apps/api/internal/validator"
|
||||||
|
"tianyuan-api/apps/api/internal/westmodel"
|
||||||
|
"tianyuan-api/pkg/crypto"
|
||||||
|
"tianyuan-api/pkg/errs"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/core/logx"
|
||||||
|
)
|
||||||
|
|
||||||
|
type COMB86PMLogic struct {
|
||||||
|
logx.Logger
|
||||||
|
ctx context.Context
|
||||||
|
svcCtx *svc.ServiceContext
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCOMB86PMLogic(ctx context.Context, svcCtx *svc.ServiceContext) *COMB86PMLogic {
|
||||||
|
return &COMB86PMLogic{
|
||||||
|
Logger: logx.WithContext(ctx),
|
||||||
|
ctx: ctx,
|
||||||
|
svcCtx: svcCtx,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *COMB86PMLogic) COMB86PM(req *types.Request) (resp string, err *errs.AppError) {
|
||||||
|
var status string
|
||||||
|
var charges bool
|
||||||
|
var remark = ""
|
||||||
|
secretKey, ok := l.ctx.Value("secretKey").(string)
|
||||||
|
if !ok {
|
||||||
|
return "", errs.ErrSystem
|
||||||
|
}
|
||||||
|
transactionID, ok := l.ctx.Value("transactionID").(string)
|
||||||
|
if !ok {
|
||||||
|
return "", errs.ErrSystem
|
||||||
|
}
|
||||||
|
userId, userIdOk := l.ctx.Value("userId").(int64)
|
||||||
|
if !userIdOk {
|
||||||
|
return "", errs.ErrSystem
|
||||||
|
}
|
||||||
|
productCode, productCodeOk := l.ctx.Value("productCode").(string)
|
||||||
|
if !productCodeOk || productCode == "" {
|
||||||
|
return "", errs.ErrSystem
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if err != nil {
|
||||||
|
status = "failed"
|
||||||
|
charges = false
|
||||||
|
} else {
|
||||||
|
status = "success"
|
||||||
|
charges = true
|
||||||
|
}
|
||||||
|
sendApiRequestMessageErr := l.svcCtx.ApiRequestMqsService.SendApiRequestMessage(l.ctx, transactionID, userId, productCode, status, charges, remark)
|
||||||
|
if sendApiRequestMessageErr != nil {
|
||||||
|
logx.Errorf("发送 API 请求消息失败: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
// 1、解密
|
||||||
|
key, decodeErr := hex.DecodeString(secretKey)
|
||||||
|
if decodeErr != nil {
|
||||||
|
return "", errs.ErrSystem
|
||||||
|
}
|
||||||
|
decryptData, aesDecryptErr := crypto.AesDecrypt(req.Data, key)
|
||||||
|
if aesDecryptErr != nil || len(decryptData) == 0 {
|
||||||
|
return "", errs.ErrParamDecryption
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2、校验
|
||||||
|
var data validator.COMB86PMRequest
|
||||||
|
if validatorErr := validator.ValidateAndParse(decryptData, &data); validatorErr != nil {
|
||||||
|
return "", errs.ErrParamValidation
|
||||||
|
}
|
||||||
|
|
||||||
|
// 准备并发请求
|
||||||
|
apiRequests := []APIRequest{
|
||||||
|
{SourceId: "G31BJ05", ServiceId: "FLXG9687", Mapping: westmodel.FLXG9687FieldMapping, Wrap: "data", Service: "west"}, // 电诈风险预警
|
||||||
|
{SourceId: "RIS031", ServiceId: "FLXG0687", Mapping: westmodel.FLXG0687FieldMapping, Wrap: "", Service: "yushan"}, // 反赌反诈
|
||||||
|
{SourceId: "MOB032", ServiceId: "FLXG54F5", Mapping: westmodel.FLXG54F5FieldMapping, Wrap: "", Service: "yushan"}, // 手机号码风险
|
||||||
|
{SourceId: "G22SC01", ServiceId: "FLXG0V4B", Mapping: westmodel.FLXG0V4BFieldMapping, Wrap: "data", Service: "west"}, // 个人司法涉诉(详版)
|
||||||
|
{SourceId: "G27BJ05", ServiceId: "JRZQ0A03", Mapping: westmodel.JRZQ0A03FieldMapping, Wrap: "data", Service: "west"}, // 借贷意向
|
||||||
|
{SourceId: "G28BJ05", ServiceId: "JRZQ8203", Mapping: westmodel.JRZQ8203FieldMapping, Wrap: "data", Service: "west"}, // 借贷行为
|
||||||
|
{SourceId: "G26BJ05", ServiceId: "FLXG3D56", Mapping: westmodel.FLXG3D56FieldMapping, Wrap: "data", Service: "west"}, // 特殊名单
|
||||||
|
{SourceId: "G09XM02", ServiceId: "IVYZ5733", Mapping: westmodel.IVYZ5733FieldMapping, Wrap: "data", Service: "west"}, // 单人婚姻
|
||||||
|
{SourceId: "G15BJ02", ServiceId: "YYSY6F2E", Mapping: westmodel.YYSY6F2EFieldMapping, Wrap: "data", Service: "west"}, // 运营商三要素(高级版)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 为每个请求构建对应的请求参数
|
||||||
|
for i := range apiRequests {
|
||||||
|
if apiRequests[i].Service == "west" {
|
||||||
|
// 西部服务:先加密后mapping
|
||||||
|
westConfig := l.svcCtx.Config.WestConfig
|
||||||
|
if apiRequests[i].SourceId == "G05HZ01" {
|
||||||
|
// G05HZ01 不需要加密,直接使用原始数据
|
||||||
|
dataMap, err := common.StructToMap(data)
|
||||||
|
if err != nil {
|
||||||
|
logx.Errorf("结构体转map失败:%v", err)
|
||||||
|
return "", errs.ErrSystem
|
||||||
|
}
|
||||||
|
logx.Infof("G05HZ01 原始数据: %+v", data)
|
||||||
|
logx.Infof("G05HZ01 转换后数据: %+v", dataMap)
|
||||||
|
apiRequests[i].Request = common.MapStructToAPIRequest(dataMap, apiRequests[i].Mapping, apiRequests[i].Wrap)
|
||||||
|
logx.Infof("G05HZ01 最终请求数据: %+v", apiRequests[i].Request)
|
||||||
|
} else {
|
||||||
|
// 其他西部服务需要加密
|
||||||
|
encryptedRequest, encryptErr := common.EncryptStructFields(data, westConfig.Key)
|
||||||
|
if encryptErr != nil {
|
||||||
|
logx.Errorf("西部加密错误:%v", encryptErr)
|
||||||
|
return "", errs.ErrSystem
|
||||||
|
}
|
||||||
|
apiRequests[i].Request = common.MapStructToAPIRequest(encryptedRequest, apiRequests[i].Mapping, apiRequests[i].Wrap)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 如果是 RIS031,添加 type: 3
|
||||||
|
if apiRequests[i].SourceId == "RIS031" {
|
||||||
|
apiRequests[i].Request = map[string]interface{}{
|
||||||
|
"keyWord": data.IDCard,
|
||||||
|
"type": 3,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if apiRequests[i].SourceId == "MOB032" {
|
||||||
|
apiRequests[i].Request = map[string]interface{}{
|
||||||
|
"mobile": data.MobileNo,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logx.Infof("sourceId:%s,请求参数:%v", apiRequests[i].SourceId, apiRequests[i].Request)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建响应通道
|
||||||
|
responseChan := make(chan APIResponse, len(apiRequests))
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
|
// 并发处理请求
|
||||||
|
for _, apiReq := range apiRequests {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(req APIRequest) {
|
||||||
|
defer wg.Done()
|
||||||
|
success := true
|
||||||
|
var westResp string
|
||||||
|
var callAPIErr *errs.AppError
|
||||||
|
|
||||||
|
// 根据服务类型选择不同的调用方式
|
||||||
|
switch req.Service {
|
||||||
|
case "west":
|
||||||
|
// 4、发送请求到西部
|
||||||
|
logx.Infof("交易号:%s", transactionID)
|
||||||
|
var respData []byte
|
||||||
|
if req.SourceId == "G05HZ01" {
|
||||||
|
respData, callAPIErr = l.svcCtx.WestDexService.CallAPISecond(req.SourceId, req.Request, l.svcCtx.Config.WestConfig.SecretSecondId)
|
||||||
|
} else {
|
||||||
|
respData, callAPIErr = l.svcCtx.WestDexService.CallAPI(req.SourceId, req.Request, l.svcCtx.Config.WestConfig.SecretId)
|
||||||
|
}
|
||||||
|
if callAPIErr != nil {
|
||||||
|
if callAPIErr.Code == errs.ErrDataSource.Code {
|
||||||
|
// 数据源错误(如查询无结果)是业务正常情况,记录为 info
|
||||||
|
logx.Infof("西部请求业务状态:sourceId:%s, resp:%v", req.SourceId, respData)
|
||||||
|
var jsonCheck interface{}
|
||||||
|
if json.Unmarshal(respData, &jsonCheck) != nil {
|
||||||
|
westResp = "{}"
|
||||||
|
} else {
|
||||||
|
westResp = string(respData)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 其他业务错误
|
||||||
|
logx.Errorf("西部请求业务错误:sourceId:%s,err:%v, resp:%v", req.SourceId, callAPIErr, respData)
|
||||||
|
westResp = "{}"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 确保返回的是有效的 JSON
|
||||||
|
if len(respData) == 0 {
|
||||||
|
westResp = "{}"
|
||||||
|
} else {
|
||||||
|
// 验证 JSON 是否有效
|
||||||
|
var jsonCheck interface{}
|
||||||
|
if json.Unmarshal(respData, &jsonCheck) != nil {
|
||||||
|
westResp = "{}"
|
||||||
|
} else {
|
||||||
|
if req.SourceId == "G22SC01" {
|
||||||
|
resultResp, parseErr := common.ParseJsonResponse(respData)
|
||||||
|
if parseErr != nil {
|
||||||
|
westResp = "{}"
|
||||||
|
} else {
|
||||||
|
westResp = string(resultResp) // 用于后续的 JSON 验证和处理
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
westResp = string(respData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "yushan":
|
||||||
|
respData, err := l.svcCtx.YushanService.Request(req.SourceId, req.Request)
|
||||||
|
if err != nil {
|
||||||
|
logx.Errorf("羽山请求失败:sourceId:%s,err:%v", req.SourceId, err)
|
||||||
|
if appErr, ok := err.(*errs.AppError); ok {
|
||||||
|
callAPIErr = appErr
|
||||||
|
} else {
|
||||||
|
callAPIErr = errs.ErrSystem
|
||||||
|
}
|
||||||
|
westResp = "{}" // 发生错误时返回空对象
|
||||||
|
} else {
|
||||||
|
// 确保返回的是有效的 JSON
|
||||||
|
if len(respData) == 0 {
|
||||||
|
westResp = "{}"
|
||||||
|
} else {
|
||||||
|
// 验证 JSON 是否有效
|
||||||
|
var jsonCheck interface{}
|
||||||
|
if json.Unmarshal(respData, &jsonCheck) != nil {
|
||||||
|
westResp = "{}"
|
||||||
|
} else {
|
||||||
|
westResp = string(respData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
success = false
|
||||||
|
westResp = "{}"
|
||||||
|
logx.Errorf("未知的服务类型:%s", req.Service)
|
||||||
|
}
|
||||||
|
|
||||||
|
if callAPIErr != nil {
|
||||||
|
success = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保响应是有效的 JSON
|
||||||
|
var jsonResp json.RawMessage
|
||||||
|
if err := json.Unmarshal([]byte(westResp), &jsonResp); err != nil {
|
||||||
|
jsonResp = json.RawMessage("{}")
|
||||||
|
}
|
||||||
|
|
||||||
|
responseChan <- APIResponse{
|
||||||
|
ServiceId: req.ServiceId,
|
||||||
|
Resp: jsonResp,
|
||||||
|
Success: success,
|
||||||
|
}
|
||||||
|
}(apiReq)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 等待所有请求完成
|
||||||
|
go func() {
|
||||||
|
wg.Wait()
|
||||||
|
close(responseChan)
|
||||||
|
}()
|
||||||
|
|
||||||
|
// 处理响应
|
||||||
|
var responses []APIResponse
|
||||||
|
for resp := range responseChan {
|
||||||
|
responses = append(responses, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
responseData := ResponseData{
|
||||||
|
Responses: make([]struct {
|
||||||
|
ServiceId string `json:"api_code"`
|
||||||
|
Data json.RawMessage `json:"data"`
|
||||||
|
Success bool `json:"success"`
|
||||||
|
}, len(responses)),
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, resp := range responses {
|
||||||
|
responseData.Responses[i] = struct {
|
||||||
|
ServiceId string `json:"api_code"`
|
||||||
|
Data json.RawMessage `json:"data"`
|
||||||
|
Success bool `json:"success"`
|
||||||
|
}{
|
||||||
|
ServiceId: resp.ServiceId,
|
||||||
|
Data: resp.Resp,
|
||||||
|
Success: resp.Success,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将响应数据转换为JSON
|
||||||
|
jsonData, marshalErr := json.Marshal(responseData)
|
||||||
|
if marshalErr != nil {
|
||||||
|
logx.Errorf("JSON编码错误:%v", marshalErr)
|
||||||
|
return "", errs.ErrSystem
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加密JSON数据
|
||||||
|
encryptData, aesEncrypt := crypto.AesEncrypt(jsonData, key)
|
||||||
|
if aesEncrypt != nil {
|
||||||
|
return "", errs.ErrSystem
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(encryptData), nil
|
||||||
|
}
|
||||||
100
apps/api/internal/logic/FLXG/flxg0687logic.go
Normal file
100
apps/api/internal/logic/FLXG/flxg0687logic.go
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
package FLXG
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/hex"
|
||||||
|
|
||||||
|
"tianyuan-api/apps/api/internal/svc"
|
||||||
|
"tianyuan-api/apps/api/internal/types"
|
||||||
|
"tianyuan-api/apps/api/internal/validator"
|
||||||
|
"tianyuan-api/pkg/crypto"
|
||||||
|
"tianyuan-api/pkg/errs"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/core/logx"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FLXG0687Logic struct {
|
||||||
|
logx.Logger
|
||||||
|
ctx context.Context
|
||||||
|
svcCtx *svc.ServiceContext
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFLXG0687Logic(ctx context.Context, svcCtx *svc.ServiceContext) *FLXG0687Logic {
|
||||||
|
return &FLXG0687Logic{
|
||||||
|
Logger: logx.WithContext(ctx),
|
||||||
|
ctx: ctx,
|
||||||
|
svcCtx: svcCtx,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *FLXG0687Logic) FLXG0687(req *types.Request) (resp string, err *errs.AppError) {
|
||||||
|
var status string
|
||||||
|
var charges bool
|
||||||
|
var remark = ""
|
||||||
|
secretKey, ok := l.ctx.Value("secretKey").(string)
|
||||||
|
if !ok {
|
||||||
|
return "", errs.ErrSystem
|
||||||
|
}
|
||||||
|
transactionID, ok := l.ctx.Value("transactionID").(string)
|
||||||
|
if !ok {
|
||||||
|
return "", errs.ErrSystem
|
||||||
|
}
|
||||||
|
userId, userIdOk := l.ctx.Value("userId").(int64)
|
||||||
|
if !userIdOk {
|
||||||
|
return "", errs.ErrSystem
|
||||||
|
}
|
||||||
|
productCode, productCodeOk := l.ctx.Value("productCode").(string)
|
||||||
|
if !productCodeOk || productCode == "" {
|
||||||
|
return "", errs.ErrSystem
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if err != nil {
|
||||||
|
status = "failed"
|
||||||
|
charges = false
|
||||||
|
} else {
|
||||||
|
status = "success"
|
||||||
|
charges = true
|
||||||
|
}
|
||||||
|
sendApiRequestMessageErr := l.svcCtx.ApiRequestMqsService.SendApiRequestMessage(l.ctx, transactionID, userId, productCode, status, charges, remark)
|
||||||
|
if sendApiRequestMessageErr != nil {
|
||||||
|
logx.Errorf("发送 API 请求消息失败: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
// 1、解密
|
||||||
|
key, decodeErr := hex.DecodeString(secretKey)
|
||||||
|
if decodeErr != nil {
|
||||||
|
return "", errs.ErrSystem
|
||||||
|
}
|
||||||
|
decryptData, aesDecryptErr := crypto.AesDecrypt(req.Data, key)
|
||||||
|
if aesDecryptErr != nil || len(decryptData) == 0 {
|
||||||
|
return "", errs.ErrParamDecryption
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2、校验
|
||||||
|
var data validator.FLXG0687Request
|
||||||
|
if validatorErr := validator.ValidateAndParse(decryptData, &data); validatorErr != nil {
|
||||||
|
return "", errs.ErrParamValidation
|
||||||
|
}
|
||||||
|
// 3、组装羽山请求参数
|
||||||
|
yushanReq := map[string]interface{}{
|
||||||
|
"keyWord": data.IDCard,
|
||||||
|
"type": 3,
|
||||||
|
}
|
||||||
|
respData, reqErr := l.svcCtx.YushanService.Request("RIS031", yushanReq)
|
||||||
|
if reqErr != nil {
|
||||||
|
logx.Errorf("羽山 RIS031 请求失败:err:%v", reqErr)
|
||||||
|
if appErr, ok := reqErr.(*errs.AppError); ok {
|
||||||
|
return "", appErr
|
||||||
|
}
|
||||||
|
return "", errs.ErrSystem
|
||||||
|
}
|
||||||
|
if len(respData) == 0 {
|
||||||
|
return "", errs.ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
encryptData, aesEncrypt := crypto.AesEncrypt(respData, key)
|
||||||
|
if aesEncrypt != nil {
|
||||||
|
return "", errs.ErrSystem
|
||||||
|
}
|
||||||
|
return encryptData, nil
|
||||||
|
}
|
||||||
@@ -88,7 +88,7 @@ func (l *FLXG5876Logic) FLXG5876(req *types.Request) (resp string, err *errs.App
|
|||||||
|
|
||||||
// 4、发送请求到西部
|
// 4、发送请求到西部
|
||||||
logx.Infof("交易号:%s", transactionID)
|
logx.Infof("交易号:%s", transactionID)
|
||||||
apiRequest := common.MapStructToAPIRequest(encryptedFields, westmodel.FLXG5876FieldMapping, "")
|
apiRequest := common.MapStructToAPIRequest(encryptedFields, westmodel.FLXG5876FieldMapping, "data")
|
||||||
|
|
||||||
westResp, callAPIErr := l.svcCtx.WestDexService.CallAPI("G03XM02", apiRequest, l.svcCtx.Config.WestConfig.SecretId)
|
westResp, callAPIErr := l.svcCtx.WestDexService.CallAPI("G03XM02", apiRequest, l.svcCtx.Config.WestConfig.SecretId)
|
||||||
if callAPIErr != nil {
|
if callAPIErr != nil {
|
||||||
|
|||||||
@@ -2,10 +2,15 @@ package IVYZ
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/hex"
|
||||||
|
"tianyuan-api/pkg/crypto"
|
||||||
"tianyuan-api/pkg/errs"
|
"tianyuan-api/pkg/errs"
|
||||||
|
|
||||||
|
"tianyuan-api/apps/api/internal/common"
|
||||||
"tianyuan-api/apps/api/internal/svc"
|
"tianyuan-api/apps/api/internal/svc"
|
||||||
"tianyuan-api/apps/api/internal/types"
|
"tianyuan-api/apps/api/internal/types"
|
||||||
|
"tianyuan-api/apps/api/internal/validator"
|
||||||
|
"tianyuan-api/apps/api/internal/westmodel"
|
||||||
|
|
||||||
"github.com/zeromicro/go-zero/core/logx"
|
"github.com/zeromicro/go-zero/core/logx"
|
||||||
)
|
)
|
||||||
@@ -25,89 +30,81 @@ func NewIVYZ0B03Logic(ctx context.Context, svcCtx *svc.ServiceContext) *IVYZ0B03
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (l *IVYZ0B03Logic) IVYZ0B03(req *types.Request) (resp string, err *errs.AppError) {
|
func (l *IVYZ0B03Logic) IVYZ0B03(req *types.Request) (resp string, err *errs.AppError) {
|
||||||
return
|
var status string
|
||||||
//var status string
|
var charges bool
|
||||||
//var charges bool
|
var remark = ""
|
||||||
//var remark = ""
|
secretKey, ok := l.ctx.Value("secretKey").(string)
|
||||||
//secretKey, ok := l.ctx.Value("secretKey").(string)
|
if !ok {
|
||||||
//if !ok {
|
return "", errs.ErrSystem
|
||||||
// return "", errs.ErrSystem
|
}
|
||||||
//}
|
transactionID, ok := l.ctx.Value("transactionID").(string)
|
||||||
//transactionID, ok := l.ctx.Value("transactionID").(string)
|
if !ok {
|
||||||
//if !ok {
|
return "", errs.ErrSystem
|
||||||
// return "", errs.ErrSystem
|
}
|
||||||
//}
|
userId, userIdOk := l.ctx.Value("userId").(int64)
|
||||||
//userId, userIdOk := l.ctx.Value("userId").(int64)
|
if !userIdOk {
|
||||||
//if !userIdOk {
|
return "", errs.ErrSystem
|
||||||
// return "", errs.ErrSystem
|
}
|
||||||
//}
|
productCode, productCodeOk := l.ctx.Value("productCode").(string)
|
||||||
//productCode, productCodeOk := l.ctx.Value("productCode").(string)
|
if !productCodeOk || productCode == "" {
|
||||||
//if !productCodeOk || productCode == "" {
|
return "", errs.ErrSystem
|
||||||
// return "", errs.ErrSystem
|
}
|
||||||
//}
|
defer func() {
|
||||||
//defer func() {
|
if err != nil {
|
||||||
// if err != nil {
|
status = "failed"
|
||||||
// status = "failed"
|
charges = false
|
||||||
// charges = false
|
} else {
|
||||||
// } else {
|
status = "success"
|
||||||
// status = "success"
|
charges = true
|
||||||
// charges = true
|
}
|
||||||
// }
|
sendApiRequestMessageErr := l.svcCtx.ApiRequestMqsService.SendApiRequestMessage(l.ctx, transactionID, userId, productCode, status, charges, remark)
|
||||||
// sendApiRequestMessageErr := l.svcCtx.ApiRequestMqsService.SendApiRequestMessage(l.ctx, transactionID, userId, productCode, status, charges, remark)
|
if sendApiRequestMessageErr != nil {
|
||||||
// if sendApiRequestMessageErr != nil {
|
logx.Errorf("发送 API 请求消息失败: %v", err)
|
||||||
// logx.Errorf("发送 API 请求消息失败: %v", err)
|
}
|
||||||
// }
|
}()
|
||||||
//}()
|
// 1、解密
|
||||||
//// 1、解密
|
key, decodeErr := hex.DecodeString(secretKey)
|
||||||
//key, decodeErr := hex.DecodeString(secretKey)
|
if decodeErr != nil {
|
||||||
//if decodeErr != nil {
|
return "", errs.ErrSystem
|
||||||
// return "", errs.ErrSystem
|
}
|
||||||
//}
|
decryptData, aesDecryptErr := crypto.AesDecrypt(req.Data, key)
|
||||||
//decryptData, aesDecryptErr := crypto.AesDecrypt(req.Data, key)
|
if aesDecryptErr != nil || len(decryptData) == 0 {
|
||||||
//if aesDecryptErr != nil || len(decryptData) == 0 {
|
return "", errs.ErrParamDecryption
|
||||||
// return "", errs.ErrParamDecryption
|
}
|
||||||
//}
|
|
||||||
//
|
// 2、校验
|
||||||
//// 2、校验
|
var data validator.IVYZ0b03Request
|
||||||
//var data validator.FLXGDEC7Request
|
if validatorErr := validator.ValidateAndParse(decryptData, &data); validatorErr != nil {
|
||||||
//if validatorErr := validator.ValidateAndParse(decryptData, &data); validatorErr != nil {
|
return "", errs.ErrParamValidation
|
||||||
// return "", errs.ErrParamValidation
|
}
|
||||||
//}
|
|
||||||
//
|
// 3、西部加密
|
||||||
//// 3、西部加密
|
westConfig := l.svcCtx.Config.WestConfig
|
||||||
//westConfig := l.svcCtx.Config.WestConfig
|
encryptedFields, encryptStructFieldsErr := common.EncryptStructFields(data, westConfig.Key)
|
||||||
//encryptedFields, encryptStructFieldsErr := common.EncryptStructFields(data, westConfig.Key)
|
if encryptStructFieldsErr != nil {
|
||||||
//if encryptStructFieldsErr != nil {
|
logx.Errorf("西部加密错误:%v", encryptStructFieldsErr)
|
||||||
// logx.Errorf("西部加密错误:%v", encryptStructFieldsErr)
|
return "", errs.ErrSystem
|
||||||
// return "", errs.ErrSystem
|
}
|
||||||
//}
|
|
||||||
//
|
// 4、发送请求到西部
|
||||||
//// 4、发送请求到西部
|
logx.Infof("交易号:%s", transactionID)
|
||||||
//logx.Infof("交易号:%s", transactionID)
|
apiRequest := common.MapStructToAPIRequest(encryptedFields, westmodel.IVYZ0b03FieldMapping, "data")
|
||||||
//apiRequest := common.MapStructToAPIRequest(encryptedFields, westmodel.FLXGDEC7FieldMapping, "data")
|
|
||||||
//
|
westResp, callAPIErr := l.svcCtx.WestDexService.CallAPI("G17BJ02", apiRequest, l.svcCtx.Config.WestConfig.SecretId)
|
||||||
//westResp, callAPIErr := l.svcCtx.WestDexService.CallAPI("G23BJ03", apiRequest, l.svcCtx.Config.WestConfig.SecretId)
|
if callAPIErr != nil {
|
||||||
//if callAPIErr != nil {
|
if callAPIErr.Code == errs.ErrDataSource.Code {
|
||||||
// return "", errs.ErrSystem
|
encryptData, aesEncrypt := crypto.AesEncrypt(westResp, key)
|
||||||
//}
|
if aesEncrypt != nil {
|
||||||
//
|
return "", errs.ErrSystem
|
||||||
//// 5、响应解析
|
}
|
||||||
////var respData westmodel.G32BJ05Response
|
return encryptData, callAPIErr
|
||||||
////unmarshalErr := json.Unmarshal(westResp, &respData)
|
}
|
||||||
////if unmarshalErr != nil {
|
return "", callAPIErr
|
||||||
//// return "", errs.ErrSystem
|
}
|
||||||
////}
|
|
||||||
////
|
encryptData, aesEncrypt := crypto.AesEncrypt(westResp, key)
|
||||||
////if respData.Data.Code == "00" || respData.Data.Code == "100002" {
|
if aesEncrypt != nil {
|
||||||
//// l.ctx = context.WithValue(l.ctx, "Charges", true)
|
return "", errs.ErrSystem
|
||||||
////} else {
|
}
|
||||||
//// return "", errs.ErrSystem
|
return encryptData, nil
|
||||||
////}
|
}
|
||||||
////encryptData, aesEncrypt := crypto.AesEncrypt(westResp, key)
|
|
||||||
////if aesEncrypt != nil {
|
|
||||||
//// return "", errs.ErrSystem
|
|
||||||
////}
|
|
||||||
//return &types.Response{
|
|
||||||
// Data: string(westResp),
|
|
||||||
//}, nil
|
|
||||||
}
|
|
||||||
@@ -3,11 +3,14 @@ package IVYZ
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"errors"
|
"encoding/json"
|
||||||
"tianyuan-api/apps/api/internal/service"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"tianyuan-api/apps/api/internal/common"
|
||||||
"tianyuan-api/apps/api/internal/svc"
|
"tianyuan-api/apps/api/internal/svc"
|
||||||
"tianyuan-api/apps/api/internal/types"
|
"tianyuan-api/apps/api/internal/types"
|
||||||
"tianyuan-api/apps/api/internal/validator"
|
"tianyuan-api/apps/api/internal/validator"
|
||||||
|
"tianyuan-api/apps/api/internal/westmodel"
|
||||||
"tianyuan-api/pkg/crypto"
|
"tianyuan-api/pkg/crypto"
|
||||||
"tianyuan-api/pkg/errs"
|
"tianyuan-api/pkg/errs"
|
||||||
|
|
||||||
@@ -49,6 +52,7 @@ func (l *IVYZ5733Logic) IVYZ5733(req *types.Request) (resp string, err *errs.App
|
|||||||
if !productCodeOk || productCode == "" {
|
if !productCodeOk || productCode == "" {
|
||||||
return "", errs.ErrSystem
|
return "", errs.ErrSystem
|
||||||
}
|
}
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
status = "failed"
|
status = "failed"
|
||||||
@@ -79,68 +83,60 @@ func (l *IVYZ5733Logic) IVYZ5733(req *types.Request) (resp string, err *errs.App
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 3、西部加密
|
// 3、西部加密
|
||||||
// westConfig := l.svcCtx.Config.WestConfig
|
westConfig := l.svcCtx.Config.WestConfig
|
||||||
// encryptedFields, encryptStructFieldsErr := common.EncryptStructFields(data, westConfig.Key)
|
encryptedFields, encryptStructFieldsErr := common.EncryptStructFields(data, westConfig.Key)
|
||||||
// if encryptStructFieldsErr != nil {
|
if encryptStructFieldsErr != nil {
|
||||||
// logx.Errorf("西部加密错误:%v", encryptStructFieldsErr)
|
logx.Errorf("西部加密错误:%v", encryptStructFieldsErr)
|
||||||
// return "", errs.ErrSystem
|
return "", errs.ErrSystem
|
||||||
// }
|
}
|
||||||
|
|
||||||
// 4、发送请求到西部
|
// 4、发送请求到西部
|
||||||
logx.Infof("交易号:%s", transactionID)
|
logx.Infof("交易号:%s", transactionID)
|
||||||
// apiRequest := common.MapStructToAPIRequest(encryptedFields, westmodel.IVYZ5733FieldMapping, "data")
|
apiRequest := common.MapStructToAPIRequest(encryptedFields, westmodel.IVYZ5733FieldMapping, "data")
|
||||||
|
|
||||||
// westResp, callAPIErr := l.svcCtx.WestDexService.CallAPI("G09SC02", apiRequest, l.svcCtx.Config.WestConfig.SecretId)
|
westResp, callAPIErr := l.svcCtx.WestDexService.CallAPI("G09XM02", apiRequest, l.svcCtx.Config.WestConfig.SecretId)
|
||||||
// if callAPIErr != nil {
|
|
||||||
// if callAPIErr.Code == errs.ErrDataSource.Code {
|
|
||||||
// encryptData, aesEncrypt := crypto.AesEncrypt(westResp, key)
|
|
||||||
// if aesEncrypt != nil {
|
|
||||||
// return "", errs.ErrSystem
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return encryptData, callAPIErr
|
|
||||||
// }
|
|
||||||
// return "", callAPIErr
|
|
||||||
// }
|
|
||||||
apiRequest := map[string]interface{}{
|
|
||||||
"cardNo": data.IDCard,
|
|
||||||
"name": data.Name,
|
|
||||||
}
|
|
||||||
respData, callAPIErr := l.svcCtx.YushanService.Request("IDV044", apiRequest)
|
|
||||||
if callAPIErr != nil {
|
if callAPIErr != nil {
|
||||||
if errors.Is(callAPIErr, service.NotFound) {
|
if callAPIErr.Code == errs.ErrDataSource.Code {
|
||||||
return "", errs.ErrNotFound
|
return "", callAPIErr
|
||||||
}
|
}
|
||||||
return "", errs.ErrSystem
|
return "", callAPIErr
|
||||||
}
|
}
|
||||||
// 使用gjson判断respData是否有status字段
|
encryptData, aesEncrypt := crypto.AesEncrypt(westResp, key)
|
||||||
statusField := gjson.GetBytes(respData, "status")
|
|
||||||
if !statusField.Exists() {
|
|
||||||
logx.Errorf("羽山返回数据中缺少status字段: %s", string(respData))
|
|
||||||
return "", errs.ErrNotFound
|
|
||||||
}
|
|
||||||
encryptData, aesEncrypt := crypto.AesEncrypt(respData, key)
|
|
||||||
if aesEncrypt != nil {
|
if aesEncrypt != nil {
|
||||||
return "", errs.ErrSystem
|
return "", errs.ErrSystem
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解析加密后的数据
|
|
||||||
// var response map[string]interface{}
|
|
||||||
// unmarshalErr := json.Unmarshal([]byte(encryptData), &response)
|
|
||||||
// if unmarshalErr != nil {
|
|
||||||
// return "", errs.ErrSystem
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // 判断是否包含 "data" 字段
|
|
||||||
// responseData, dataOk := response["data"].([]interface{})
|
|
||||||
// if !dataOk || len(responseData) == 0 {
|
|
||||||
// return "", errs.ErrSystem
|
|
||||||
// }
|
|
||||||
|
|
||||||
// 判断 "data" 中是否包含 "maritalStatus"
|
|
||||||
// maritalStatus := response.(map[string]interface{})["maritalStatus"]
|
|
||||||
// if maritalStatus == nil {
|
|
||||||
// return "", errs.ErrSystem
|
|
||||||
// }
|
|
||||||
return encryptData, nil
|
return encryptData, nil
|
||||||
}
|
}
|
||||||
|
func handleResponse(resp []byte) ([]byte, error) {
|
||||||
|
result := gjson.GetBytes(resp, "data.data")
|
||||||
|
if !result.Exists() {
|
||||||
|
return nil, fmt.Errorf("婚姻状态查询失败")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取原始结果
|
||||||
|
rawResult := result.String()
|
||||||
|
|
||||||
|
// 根据结果转换状态码
|
||||||
|
var statusCode string
|
||||||
|
switch {
|
||||||
|
case strings.HasPrefix(rawResult, "INR"):
|
||||||
|
statusCode = "0" // 匹配不成功
|
||||||
|
case strings.HasPrefix(rawResult, "IA"):
|
||||||
|
statusCode = "1" // 结婚
|
||||||
|
case strings.HasPrefix(rawResult, "IB"):
|
||||||
|
statusCode = "2" // 离婚
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("婚姻状态查询失败,未知状态码: %s", statusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建新的返回结果
|
||||||
|
response := map[string]string{
|
||||||
|
"status": statusCode,
|
||||||
|
}
|
||||||
|
// 序列化为JSON
|
||||||
|
jsonResponse, err := json.Marshal(response)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("序列化结果失败: %v", err)
|
||||||
|
}
|
||||||
|
return jsonResponse, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,11 +3,13 @@ package YYSY
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
"tianyuan-api/apps/api/internal/common"
|
"tianyuan-api/apps/api/internal/common"
|
||||||
"tianyuan-api/apps/api/internal/validator"
|
"tianyuan-api/apps/api/internal/validator"
|
||||||
"tianyuan-api/apps/api/internal/westmodel"
|
"tianyuan-api/apps/api/internal/westmodel"
|
||||||
"tianyuan-api/pkg/crypto"
|
"tianyuan-api/pkg/crypto"
|
||||||
"tianyuan-api/pkg/errs"
|
"tianyuan-api/pkg/errs"
|
||||||
|
"time"
|
||||||
|
|
||||||
"tianyuan-api/apps/api/internal/svc"
|
"tianyuan-api/apps/api/internal/svc"
|
||||||
"tianyuan-api/apps/api/internal/types"
|
"tianyuan-api/apps/api/internal/types"
|
||||||
@@ -89,8 +91,9 @@ func (l *YYSYBE08Logic) YYSYBE08(req *types.Request) (resp string, err *errs.App
|
|||||||
// 4、发送请求到西部
|
// 4、发送请求到西部
|
||||||
logx.Infof("交易号:%s", transactionID)
|
logx.Infof("交易号:%s", transactionID)
|
||||||
apiRequest := common.MapStructToAPIRequest(encryptedFields, westmodel.YYSYBE08FieldMapping, "data")
|
apiRequest := common.MapStructToAPIRequest(encryptedFields, westmodel.YYSYBE08FieldMapping, "data")
|
||||||
|
apiRequest["data"].(map[string]interface{})["customerNumber"] = l.svcCtx.Config.WestConfig.SecretId
|
||||||
westResp, callAPIErr := l.svcCtx.WestDexService.CallAPI("G17BJ02", apiRequest, l.svcCtx.Config.WestConfig.SecretId)
|
apiRequest["data"].(map[string]interface{})["timeStamp"] = fmt.Sprintf("%d", time.Now().UnixNano()/int64(time.Millisecond))
|
||||||
|
westResp, callAPIErr := l.svcCtx.WestDexService.CallAPI("layoutIdcard", apiRequest, l.svcCtx.Config.WestConfig.SecretId)
|
||||||
if callAPIErr != nil {
|
if callAPIErr != nil {
|
||||||
if callAPIErr.Code == errs.ErrDataSource.Code {
|
if callAPIErr.Code == errs.ErrDataSource.Code {
|
||||||
encryptData, aesEncrypt := crypto.AesEncrypt(westResp, key)
|
encryptData, aesEncrypt := crypto.AesEncrypt(westResp, key)
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ func (w *WestDexService) CallAPI(code string, reqData map[string]interface{}, se
|
|||||||
|
|
||||||
logx.Infof("西部流水号: %s", westDexResp.ID)
|
logx.Infof("西部流水号: %s", westDexResp.ID)
|
||||||
|
|
||||||
if westDexResp.Code != "00000" && westDexResp.Code != "200" {
|
if westDexResp.Code != "00000" && westDexResp.Code != "200" && westDexResp.Code != "0" {
|
||||||
if westDexResp.Data == "" {
|
if westDexResp.Data == "" {
|
||||||
logx.Errorf("【西部数据请求】业务失败时响应数据为空: %s %s", westDexResp.Message, westDexResp.Reason)
|
logx.Errorf("【西部数据请求】业务失败时响应数据为空: %s %s", westDexResp.Message, westDexResp.Reason)
|
||||||
return nil, errs.ErrSystem
|
return nil, errs.ErrSystem
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Code generated by goctl. DO NOT EDIT.
|
// Code generated by goctl. DO NOT EDIT.
|
||||||
// goctl 1.7.3
|
// goctl 1.8.4
|
||||||
|
|
||||||
package types
|
package types
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,9 @@ type FLXG162ARequest struct {
|
|||||||
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||||
Name string `json:"name" validate:"required,min=1,validName"`
|
Name string `json:"name" validate:"required,min=1,validName"`
|
||||||
}
|
}
|
||||||
|
type FLXG0687Request struct {
|
||||||
|
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||||
|
}
|
||||||
type FLXG970FRequest struct {
|
type FLXG970FRequest struct {
|
||||||
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||||
Name string `json:"name" validate:"required,min=1,validName"`
|
Name string `json:"name" validate:"required,min=1,validName"`
|
||||||
@@ -64,8 +67,6 @@ type FLXGDEC7Request struct {
|
|||||||
Name string `json:"name" validate:"required,min=1,validName"`
|
Name string `json:"name" validate:"required,min=1,validName"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type IVYZ0B03Request struct {
|
|
||||||
}
|
|
||||||
type IVYZ385ERequest struct {
|
type IVYZ385ERequest struct {
|
||||||
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||||
Name string `json:"name" validate:"required,min=1,validName"`
|
Name string `json:"name" validate:"required,min=1,validName"`
|
||||||
@@ -146,10 +147,14 @@ type YYSY09CDRequest struct {
|
|||||||
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||||
Name string `json:"name" validate:"required,min=1,validName"`
|
Name string `json:"name" validate:"required,min=1,validName"`
|
||||||
}
|
}
|
||||||
type YYSYBE08Request struct {
|
type IVYZ0b03Request struct {
|
||||||
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||||
Name string `json:"name" validate:"required,min=1,validName"`
|
Name string `json:"name" validate:"required,min=1,validName"`
|
||||||
}
|
}
|
||||||
|
type YYSYBE08Request struct{
|
||||||
|
Name string `json:"name" validate:"required,min=1,validName"`
|
||||||
|
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||||
|
}
|
||||||
type YYSYD50FRequest struct {
|
type YYSYD50FRequest struct {
|
||||||
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||||
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||||
@@ -162,3 +167,17 @@ type IVYZ9A2BRequest struct {
|
|||||||
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||||
Name string `json:"name" validate:"required,min=1,validName"`
|
Name string `json:"name" validate:"required,min=1,validName"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type COMB298YRequest struct {
|
||||||
|
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||||
|
Name string `json:"name" validate:"required,min=1,validName"`
|
||||||
|
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||||
|
AuthDate string `json:"auth_date" validate:"required,validAuthDate" encrypt:"false"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type COMB86PMRequest struct {
|
||||||
|
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||||
|
Name string `json:"name" validate:"required,min=1,validName"`
|
||||||
|
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||||
|
AuthDate string `json:"auth_date" validate:"required,validAuthDate" encrypt:"false"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -43,21 +43,25 @@ var FLXGC9D1FieldMapping = map[string]string{
|
|||||||
"MobileNo": "cell",
|
"MobileNo": "cell",
|
||||||
}
|
}
|
||||||
var FLXGCA3DFieldMapping = map[string]string{
|
var FLXGCA3DFieldMapping = map[string]string{
|
||||||
"IDCard": "id_card",
|
"IDCard": "idcard",
|
||||||
"Name": "name",
|
"Name": "name",
|
||||||
|
"AuthDate": "inquired_auth",
|
||||||
}
|
}
|
||||||
var FLXGDEC7FieldMapping = map[string]string{
|
var FLXGDEC7FieldMapping = map[string]string{
|
||||||
"IDCard": "id_card",
|
"IDCard": "id_card",
|
||||||
"Name": "name",
|
"Name": "name",
|
||||||
}
|
}
|
||||||
|
var FLXG0687FieldMapping = map[string]string{
|
||||||
|
"IDCard": "keyWord",
|
||||||
|
}
|
||||||
var IVYZ385EFieldMapping = map[string]string{
|
var IVYZ385EFieldMapping = map[string]string{
|
||||||
"IDCard": "gmsfzhm",
|
"IDCard": "gmsfzhm",
|
||||||
"Name": "xm",
|
"Name": "xm",
|
||||||
}
|
}
|
||||||
|
|
||||||
var IVYZ5733FieldMapping = map[string]string{
|
var IVYZ5733FieldMapping = map[string]string{
|
||||||
"IDCard": "certNumMan",
|
"IDCard": "idCard",
|
||||||
"Name": "nameMan",
|
"Name": "name",
|
||||||
}
|
}
|
||||||
var IVYZ9363FieldMapping = map[string]string{
|
var IVYZ9363FieldMapping = map[string]string{
|
||||||
"ManName": "nameMan",
|
"ManName": "nameMan",
|
||||||
@@ -112,6 +116,7 @@ var QYGL8271FieldMapping = map[string]string{
|
|||||||
var QYGLB4C0FieldMapping = map[string]string{
|
var QYGLB4C0FieldMapping = map[string]string{
|
||||||
"IDCard": "pid",
|
"IDCard": "pid",
|
||||||
}
|
}
|
||||||
|
|
||||||
var YYSY4B37FieldMapping = map[string]string{
|
var YYSY4B37FieldMapping = map[string]string{
|
||||||
"MobileNo": "phone",
|
"MobileNo": "phone",
|
||||||
}
|
}
|
||||||
@@ -130,10 +135,14 @@ var YYSY09CDFieldMapping = map[string]string{
|
|||||||
"MobileNo": "phone",
|
"MobileNo": "phone",
|
||||||
"MobileType": "phoneType",
|
"MobileType": "phoneType",
|
||||||
}
|
}
|
||||||
var YYSYBE08FieldMapping = map[string]string{
|
var IVYZ0b03FieldMapping = map[string]string{
|
||||||
"Name": "name",
|
"Name": "name",
|
||||||
"MobileNo": "phone",
|
"MobileNo": "phone",
|
||||||
}
|
}
|
||||||
|
var YYSYBE08FieldMapping = map[string]string{
|
||||||
|
"Name": "xM",
|
||||||
|
"IDCard": "gMSFZHM",
|
||||||
|
}
|
||||||
var YYSYD50FFieldMapping = map[string]string{
|
var YYSYD50FFieldMapping = map[string]string{
|
||||||
"MobileNo": "phone",
|
"MobileNo": "phone",
|
||||||
"IDCard": "idNo",
|
"IDCard": "idNo",
|
||||||
|
|||||||
108
apps/user/internal/logic/auth/loginuserlogic_test.go
Normal file
108
apps/user/internal/logic/auth/loginuserlogic_test.go
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
package authlogic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestHashPassword(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
password string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "空密码",
|
||||||
|
password: "",
|
||||||
|
want: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "简单密码",
|
||||||
|
password: "123456",
|
||||||
|
want: "8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "复杂密码",
|
||||||
|
password: "MyP@ssw0rd!",
|
||||||
|
want: "e493c394a28652900d73f0fc7e6713840b1af0ab1f3fd9c5878d82e5f753c6c1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "中文字符",
|
||||||
|
password: "密码123",
|
||||||
|
want: "5a75e520515bf7695cd356454c4edb05ce925e230acf6c881701b7b8444dcbed",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "特殊字符",
|
||||||
|
password: "!@#$%^&*()",
|
||||||
|
want: "95ce789c5c9d18490972709838ca3a9719094bca3ac16332cfec0652b0236141",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got := hashPassword(tt.password)
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("hashPassword() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试hashPassword函数的确定性(相同输入总是产生相同输出)
|
||||||
|
func TestHashPasswordDeterministic(t *testing.T) {
|
||||||
|
password := "testPassword123"
|
||||||
|
|
||||||
|
// 多次调用应该产生相同的结果
|
||||||
|
result1 := hashPassword(password)
|
||||||
|
result2 := hashPassword(password)
|
||||||
|
result3 := hashPassword(password)
|
||||||
|
|
||||||
|
if result1 != result2 || result2 != result3 {
|
||||||
|
t.Errorf("hashPassword() 不是确定性的: %v, %v, %v", result1, result2, result3)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试hashPassword函数的输出格式
|
||||||
|
func TestHashPasswordFormat(t *testing.T) {
|
||||||
|
password := "test"
|
||||||
|
result := hashPassword(password)
|
||||||
|
|
||||||
|
// 检查输出是否为64个字符的十六进制字符串
|
||||||
|
if len(result) != 64 {
|
||||||
|
t.Errorf("hashPassword() 输出长度不正确: got %d, want 64", len(result))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否为有效的十六进制字符串
|
||||||
|
_, err := hex.DecodeString(result)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("hashPassword() 输出不是有效的十六进制字符串: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 基准测试
|
||||||
|
func BenchmarkHashPassword(b *testing.B) {
|
||||||
|
password := "benchmarkPassword123"
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
hashPassword(password)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证我们的实现与标准库的一致性
|
||||||
|
func TestHashPasswordConsistency(t *testing.T) {
|
||||||
|
password := "consistencyTest"
|
||||||
|
|
||||||
|
// 使用我们的函数
|
||||||
|
ourResult := hashPassword(password)
|
||||||
|
|
||||||
|
// 手动计算SHA256
|
||||||
|
h := sha256.New()
|
||||||
|
h.Write([]byte(password))
|
||||||
|
expectedResult := hex.EncodeToString(h.Sum(nil))
|
||||||
|
|
||||||
|
if ourResult != expectedResult {
|
||||||
|
t.Errorf("hashPassword() 与标准库不一致: got %v, want %v", ourResult, expectedResult)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,11 +4,12 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/zeromicro/go-zero/core/stores/redis"
|
|
||||||
"github.com/zeromicro/go-zero/core/stores/sqlc"
|
|
||||||
"tianyuan-api/apps/user/internal/model"
|
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
"tianyuan-api/apps/user/internal/model"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/core/stores/redis"
|
||||||
|
"github.com/zeromicro/go-zero/core/stores/sqlc"
|
||||||
|
|
||||||
"tianyuan-api/apps/user/internal/svc"
|
"tianyuan-api/apps/user/internal/svc"
|
||||||
"tianyuan-api/apps/user/user"
|
"tianyuan-api/apps/user/user"
|
||||||
|
|||||||
389
cmd目录详解.md
Normal file
389
cmd目录详解.md
Normal file
@@ -0,0 +1,389 @@
|
|||||||
|
# Go 项目中的 cmd 目录详解
|
||||||
|
|
||||||
|
## 🎯 cmd 目录的作用
|
||||||
|
|
||||||
|
`cmd` 目录是 Go 项目的标准目录布局,专门用来存放**可执行程序的入口点**。每个子目录代表一个不同的应用程序。
|
||||||
|
|
||||||
|
## 📁 目录结构示例
|
||||||
|
|
||||||
|
```
|
||||||
|
user-service/
|
||||||
|
├── cmd/ # 应用程序入口目录
|
||||||
|
│ ├── server/ # HTTP/gRPC 服务器
|
||||||
|
│ │ └── main.go # 服务器启动入口
|
||||||
|
│ ├── cli/ # 命令行工具
|
||||||
|
│ │ └── main.go # CLI 工具入口
|
||||||
|
│ ├── worker/ # 后台任务处理器
|
||||||
|
│ │ └── main.go # Worker 入口
|
||||||
|
│ └── migrator/ # 数据库迁移工具
|
||||||
|
│ └── main.go # 迁移工具入口
|
||||||
|
├── internal/ # 内部业务逻辑
|
||||||
|
├── api/ # API 定义
|
||||||
|
└── pkg/ # 可复用的包
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 具体示例
|
||||||
|
|
||||||
|
### 1. 服务器入口 (cmd/server/main.go)
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
"user-service/internal/config"
|
||||||
|
"user-service/internal/server"
|
||||||
|
"user-service/internal/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// 1. 解析命令行参数
|
||||||
|
var (
|
||||||
|
configFile = flag.String("config", "configs/config.yaml", "配置文件路径")
|
||||||
|
port = flag.Int("port", 8080, "服务端口")
|
||||||
|
)
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
// 2. 加载配置
|
||||||
|
cfg, err := config.Load(*configFile)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("加载配置失败:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 初始化服务
|
||||||
|
userService := service.NewUserService(cfg)
|
||||||
|
|
||||||
|
// 4. 创建 gRPC 服务器
|
||||||
|
grpcServer := grpc.NewServer()
|
||||||
|
server.RegisterUserServer(grpcServer, userService)
|
||||||
|
|
||||||
|
// 5. 启动服务器
|
||||||
|
lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port))
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("监听端口失败:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. 优雅关闭
|
||||||
|
go func() {
|
||||||
|
log.Printf("用户服务启动在端口 %d", *port)
|
||||||
|
if err := grpcServer.Serve(lis); err != nil {
|
||||||
|
log.Fatal("服务启动失败:", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// 7. 等待中断信号
|
||||||
|
quit := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
<-quit
|
||||||
|
|
||||||
|
log.Println("正在关闭服务...")
|
||||||
|
grpcServer.GracefulStop()
|
||||||
|
log.Println("服务已关闭")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. CLI 工具入口 (cmd/cli/main.go)
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"user-service/internal/config"
|
||||||
|
"user-service/internal/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
var (
|
||||||
|
action = flag.String("action", "", "执行的动作: create-user, list-users, reset-password")
|
||||||
|
userID = flag.Int64("user-id", 0, "用户ID")
|
||||||
|
username = flag.String("username", "", "用户名")
|
||||||
|
)
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
cfg, err := config.Load("configs/config.yaml")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("加载配置失败: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
userService := service.NewUserService(cfg)
|
||||||
|
|
||||||
|
switch *action {
|
||||||
|
case "create-user":
|
||||||
|
if *username == "" {
|
||||||
|
fmt.Println("用户名不能为空")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
user, err := userService.CreateUser(*username)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("创建用户失败: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
fmt.Printf("用户创建成功: ID=%d, Username=%s\n", user.ID, user.Username)
|
||||||
|
|
||||||
|
case "list-users":
|
||||||
|
users, err := userService.ListUsers()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("获取用户列表失败: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
for _, user := range users {
|
||||||
|
fmt.Printf("ID: %d, Username: %s, Email: %s\n", user.ID, user.Username, user.Email)
|
||||||
|
}
|
||||||
|
|
||||||
|
case "reset-password":
|
||||||
|
if *userID == 0 {
|
||||||
|
fmt.Println("用户ID不能为空")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
newPassword, err := userService.ResetPassword(*userID)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("重置密码失败: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
fmt.Printf("密码重置成功,新密码: %s\n", newPassword)
|
||||||
|
|
||||||
|
default:
|
||||||
|
fmt.Println("不支持的操作,支持的操作: create-user, list-users, reset-password")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Worker 入口 (cmd/worker/main.go)
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"user-service/internal/config"
|
||||||
|
"user-service/internal/worker"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
cfg, err := config.Load("configs/config.yaml")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("加载配置失败:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建上下文
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// 初始化 Worker
|
||||||
|
emailWorker := worker.NewEmailWorker(cfg)
|
||||||
|
notificationWorker := worker.NewNotificationWorker(cfg)
|
||||||
|
|
||||||
|
// 启动 Workers
|
||||||
|
go func() {
|
||||||
|
log.Println("启动邮件处理 Worker...")
|
||||||
|
if err := emailWorker.Start(ctx); err != nil {
|
||||||
|
log.Printf("邮件 Worker 错误: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
log.Println("启动通知处理 Worker...")
|
||||||
|
if err := notificationWorker.Start(ctx); err != nil {
|
||||||
|
log.Printf("通知 Worker 错误: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// 优雅关闭
|
||||||
|
quit := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
<-quit
|
||||||
|
|
||||||
|
log.Println("正在关闭 Workers...")
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
// 等待 Workers 完成
|
||||||
|
time.Sleep(5 * time.Second)
|
||||||
|
log.Println("Workers 已关闭")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 数据库迁移工具 (cmd/migrator/main.go)
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"user-service/internal/config"
|
||||||
|
"user-service/internal/migration"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
var (
|
||||||
|
direction = flag.String("direction", "up", "迁移方向: up 或 down")
|
||||||
|
steps = flag.Int("steps", 0, "迁移步数,0表示全部")
|
||||||
|
)
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
cfg, err := config.Load("configs/config.yaml")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("加载配置失败:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
migrator := migration.NewMigrator(cfg.Database.URL)
|
||||||
|
|
||||||
|
switch *direction {
|
||||||
|
case "up":
|
||||||
|
log.Println("执行数据库迁移...")
|
||||||
|
if err := migrator.Up(*steps); err != nil {
|
||||||
|
log.Fatal("迁移失败:", err)
|
||||||
|
}
|
||||||
|
log.Println("迁移成功")
|
||||||
|
|
||||||
|
case "down":
|
||||||
|
log.Println("回滚数据库迁移...")
|
||||||
|
if err := migrator.Down(*steps); err != nil {
|
||||||
|
log.Fatal("回滚失败:", err)
|
||||||
|
}
|
||||||
|
log.Println("回滚成功")
|
||||||
|
|
||||||
|
default:
|
||||||
|
log.Fatal("不支持的迁移方向:", *direction)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎯 为什么这样设计?
|
||||||
|
|
||||||
|
### 1. **关注点分离**
|
||||||
|
|
||||||
|
- `cmd/` 只负责程序启动和配置解析
|
||||||
|
- `internal/` 负责具体的业务逻辑
|
||||||
|
- 每个应用程序有独立的入口点
|
||||||
|
|
||||||
|
### 2. **多种运行模式**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 启动 HTTP/gRPC 服务器
|
||||||
|
./user-service-server -port=8080 -config=prod.yaml
|
||||||
|
|
||||||
|
# 使用 CLI 工具
|
||||||
|
./user-service-cli -action=create-user -username=john
|
||||||
|
|
||||||
|
# 启动后台 Worker
|
||||||
|
./user-service-worker
|
||||||
|
|
||||||
|
# 执行数据库迁移
|
||||||
|
./user-service-migrator -direction=up
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. **构建和部署灵活性**
|
||||||
|
|
||||||
|
```dockerfile
|
||||||
|
# Dockerfile 示例
|
||||||
|
FROM golang:1.20-alpine AS builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# 分别构建不同的应用程序
|
||||||
|
RUN go build -o server ./cmd/server
|
||||||
|
RUN go build -o cli ./cmd/cli
|
||||||
|
RUN go build -o worker ./cmd/worker
|
||||||
|
RUN go build -o migrator ./cmd/migrator
|
||||||
|
|
||||||
|
FROM alpine:latest
|
||||||
|
RUN apk --no-cache add ca-certificates
|
||||||
|
WORKDIR /root/
|
||||||
|
|
||||||
|
# 根据需要复制不同的可执行文件
|
||||||
|
COPY --from=builder /app/server .
|
||||||
|
COPY --from=builder /app/migrator .
|
||||||
|
|
||||||
|
# 可以选择启动不同的程序
|
||||||
|
CMD ["./server"]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. **Makefile 示例**
|
||||||
|
|
||||||
|
```makefile
|
||||||
|
# Makefile
|
||||||
|
.PHONY: build-server build-cli build-worker build-migrator
|
||||||
|
|
||||||
|
build-server:
|
||||||
|
go build -o bin/server ./cmd/server
|
||||||
|
|
||||||
|
build-cli:
|
||||||
|
go build -o bin/cli ./cmd/cli
|
||||||
|
|
||||||
|
build-worker:
|
||||||
|
go build -o bin/worker ./cmd/worker
|
||||||
|
|
||||||
|
build-migrator:
|
||||||
|
go build -o bin/migrator ./cmd/migrator
|
||||||
|
|
||||||
|
build-all: build-server build-cli build-worker build-migrator
|
||||||
|
|
||||||
|
run-server:
|
||||||
|
./bin/server -port=8080
|
||||||
|
|
||||||
|
run-worker:
|
||||||
|
./bin/worker
|
||||||
|
|
||||||
|
migrate-up:
|
||||||
|
./bin/migrator -direction=up
|
||||||
|
|
||||||
|
migrate-down:
|
||||||
|
./bin/migrator -direction=down -steps=1
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 在你的项目中的应用
|
||||||
|
|
||||||
|
在你当前的项目中,每个服务都应该有这样的结构:
|
||||||
|
|
||||||
|
```
|
||||||
|
apps/user/
|
||||||
|
├── cmd/
|
||||||
|
│ ├── server/main.go # gRPC 服务器
|
||||||
|
│ ├── cli/main.go # 用户管理 CLI
|
||||||
|
│ └── migrator/main.go # 数据库迁移
|
||||||
|
├── internal/ # 业务逻辑
|
||||||
|
├── user.proto # API 定义
|
||||||
|
└── Dockerfile
|
||||||
|
|
||||||
|
apps/gateway/
|
||||||
|
├── cmd/
|
||||||
|
│ ├── server/main.go # HTTP 网关服务器
|
||||||
|
│ └── cli/main.go # 网关管理 CLI
|
||||||
|
├── internal/
|
||||||
|
├── gateway.api
|
||||||
|
└── Dockerfile
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📋 总结
|
||||||
|
|
||||||
|
`cmd` 目录的核心作用:
|
||||||
|
|
||||||
|
1. **程序入口点** - 每个 main.go 是一个独立的应用程序
|
||||||
|
2. **配置解析** - 处理命令行参数和配置文件
|
||||||
|
3. **依赖注入** - 初始化和连接各个组件
|
||||||
|
4. **生命周期管理** - 启动、运行、优雅关闭
|
||||||
|
5. **多种运行模式** - 服务器、CLI、Worker 等不同形态
|
||||||
744
go-zero服务实现详解.md
Normal file
744
go-zero服务实现详解.md
Normal file
@@ -0,0 +1,744 @@
|
|||||||
|
# go-zero 服务实现详解
|
||||||
|
|
||||||
|
## 🔥 核心服务实现
|
||||||
|
|
||||||
|
### 1. Gateway API 服务 (HTTP 入口)
|
||||||
|
|
||||||
|
#### API 定义 (gateway.api)
|
||||||
|
|
||||||
|
```go
|
||||||
|
syntax = "v1"
|
||||||
|
|
||||||
|
info(
|
||||||
|
title: "天远API网关"
|
||||||
|
desc: "统一API入口"
|
||||||
|
version: "v1.0"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
// 登录请求
|
||||||
|
LoginReq {
|
||||||
|
Username string `json:"username"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
}
|
||||||
|
|
||||||
|
LoginResp {
|
||||||
|
Token string `json:"token"`
|
||||||
|
UserInfo UserInfo `json:"userInfo"`
|
||||||
|
}
|
||||||
|
|
||||||
|
UserInfo {
|
||||||
|
UserId int64 `json:"userId"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
EnterpriseId int64 `json:"enterpriseId"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 数据查询请求 (你的核心业务)
|
||||||
|
DataQueryReq {
|
||||||
|
QueryType string `json:"queryType"` // risk/credit/company/data
|
||||||
|
Parameters map[string]interface{} `json:"parameters"`
|
||||||
|
}
|
||||||
|
|
||||||
|
DataQueryResp {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
Data interface{} `json:"data"`
|
||||||
|
TransactionId string `json:"transactionId"`
|
||||||
|
RemainingBalance float64 `json:"remainingBalance"`
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
@server(
|
||||||
|
jwt: Auth
|
||||||
|
group: auth
|
||||||
|
)
|
||||||
|
service gateway-api {
|
||||||
|
@handler LoginHandler
|
||||||
|
post /api/v1/auth/login (LoginReq) returns (LoginResp)
|
||||||
|
|
||||||
|
@handler LogoutHandler
|
||||||
|
post /api/v1/auth/logout returns ()
|
||||||
|
}
|
||||||
|
|
||||||
|
@server(
|
||||||
|
jwt: Auth
|
||||||
|
group: data
|
||||||
|
middleware: RateLimit,Audit
|
||||||
|
)
|
||||||
|
service gateway-api {
|
||||||
|
@handler RiskAssessmentHandler
|
||||||
|
post /api/v1/data/risk-assessment (DataQueryReq) returns (DataQueryResp)
|
||||||
|
|
||||||
|
@handler CreditCheckHandler
|
||||||
|
post /api/v1/data/credit-check (DataQueryReq) returns (DataQueryResp)
|
||||||
|
|
||||||
|
@handler CompanyInfoHandler
|
||||||
|
post /api/v1/data/company-info (DataQueryReq) returns (DataQueryResp)
|
||||||
|
|
||||||
|
@handler DataQueryHandler
|
||||||
|
post /api/v1/data/query (DataQueryReq) returns (DataQueryResp)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 核心 Logic 实现 (处理复杂调用链)
|
||||||
|
|
||||||
|
```go
|
||||||
|
// api/gateway/internal/logic/data/risklogic.go
|
||||||
|
|
||||||
|
type RiskLogic struct {
|
||||||
|
logx.Logger
|
||||||
|
ctx context.Context
|
||||||
|
svcCtx *svc.ServiceContext
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *RiskLogic) RiskAssessment(req *types.DataQueryReq) (resp *types.DataQueryResp, err error) {
|
||||||
|
// 获取用户信息 (从JWT中解析)
|
||||||
|
userId := ctxdata.GetUidFromCtx(l.ctx)
|
||||||
|
|
||||||
|
// 🔥 调用数据域RPC进行复杂业务处理
|
||||||
|
dataResp, err := l.svcCtx.DataRpc.ProcessDataRequest(l.ctx, &data.ProcessDataRequestReq{
|
||||||
|
UserId: userId,
|
||||||
|
QueryType: "risk-assessment",
|
||||||
|
Parameters: req.Parameters,
|
||||||
|
ClientIp: httpx.GetClientIP(l.ctx),
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
logx.Errorf("调用数据域RPC失败: %v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &types.DataQueryResp{
|
||||||
|
Success: dataResp.Success,
|
||||||
|
Data: dataResp.Data,
|
||||||
|
TransactionId: dataResp.TransactionId,
|
||||||
|
RemainingBalance: dataResp.RemainingBalance,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 服务上下文 (包含所有 RPC 客户端)
|
||||||
|
|
||||||
|
```go
|
||||||
|
// api/gateway/internal/svc/servicecontext.go
|
||||||
|
|
||||||
|
type ServiceContext struct {
|
||||||
|
Config config.Config
|
||||||
|
|
||||||
|
// 🔗 RPC客户端连接
|
||||||
|
UserRpc user.User
|
||||||
|
DataRpc data.Data
|
||||||
|
SecurityRpc security.Security
|
||||||
|
BillingRpc billing.Billing
|
||||||
|
ProductRpc product.Product
|
||||||
|
AuditRpc audit.Audit
|
||||||
|
|
||||||
|
// 中间件
|
||||||
|
RateLimit rest.Middleware
|
||||||
|
AuditMiddleware rest.Middleware
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewServiceContext(c config.Config) *ServiceContext {
|
||||||
|
return &ServiceContext{
|
||||||
|
Config: c,
|
||||||
|
|
||||||
|
// 初始化RPC客户端
|
||||||
|
UserRpc: user.NewUser(zrpc.MustNewClient(c.UserRpc)),
|
||||||
|
DataRpc: data.NewData(zrpc.MustNewClient(c.DataRpc)),
|
||||||
|
SecurityRpc: security.NewSecurity(zrpc.MustNewClient(c.SecurityRpc)),
|
||||||
|
BillingRpc: billing.NewBilling(zrpc.MustNewClient(c.BillingRpc)),
|
||||||
|
ProductRpc: product.NewProduct(zrpc.MustNewClient(c.ProductRpc)),
|
||||||
|
AuditRpc: audit.NewAudit(zrpc.MustNewClient(c.AuditRpc)),
|
||||||
|
|
||||||
|
// 初始化中间件
|
||||||
|
RateLimit: ratelimit.NewRateLimit(c.RateLimit),
|
||||||
|
AuditMiddleware: auditrpc.NewAuditMiddleware(c.Audit),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Data RPC 服务 (核心协调者)
|
||||||
|
|
||||||
|
#### Proto 定义 (data.proto)
|
||||||
|
|
||||||
|
```protobuf
|
||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package data;
|
||||||
|
|
||||||
|
option go_package = "./pb";
|
||||||
|
|
||||||
|
// 数据处理请求
|
||||||
|
message ProcessDataRequestReq {
|
||||||
|
int64 user_id = 1;
|
||||||
|
string query_type = 2; // risk-assessment/credit-check/company-info/data-query
|
||||||
|
map<string, string> parameters = 3;
|
||||||
|
string client_ip = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ProcessDataRequestResp {
|
||||||
|
bool success = 1;
|
||||||
|
string data = 2; // JSON格式的业务数据
|
||||||
|
string transaction_id = 3;
|
||||||
|
double remaining_balance = 4;
|
||||||
|
string error_message = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
service Data {
|
||||||
|
rpc ProcessDataRequest(ProcessDataRequestReq) returns (ProcessDataRequestResp);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 核心协调逻辑 (你的复杂业务流程)
|
||||||
|
|
||||||
|
```go
|
||||||
|
// rpc/data/internal/logic/orchestrator/dataorchestratorlogic.go
|
||||||
|
|
||||||
|
type DataOrchestratorLogic struct {
|
||||||
|
ctx context.Context
|
||||||
|
svcCtx *svc.ServiceContext
|
||||||
|
logx.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *DataOrchestratorLogic) ProcessDataRequest(in *pb.ProcessDataRequestReq) (*pb.ProcessDataRequestResp, error) {
|
||||||
|
startTime := time.Now()
|
||||||
|
|
||||||
|
// === 第1步:安全验证 ===
|
||||||
|
|
||||||
|
// 1.1 获取用户企业信息
|
||||||
|
userResp, err := l.svcCtx.UserRpc.GetUserInfo(l.ctx, &user.GetUserInfoReq{
|
||||||
|
UserId: in.UserId,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("获取用户信息失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1.2 IP白名单验证
|
||||||
|
_, err = l.svcCtx.SecurityRpc.CheckWhitelist(l.ctx, &security.CheckWhitelistReq{
|
||||||
|
EnterpriseId: userResp.EnterpriseId,
|
||||||
|
ClientIp: in.ClientIp,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("IP白名单验证失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1.3 密钥解密
|
||||||
|
decryptResp, err := l.svcCtx.SecurityRpc.DecryptSecret(l.ctx, &security.DecryptSecretReq{
|
||||||
|
EnterpriseId: userResp.EnterpriseId,
|
||||||
|
EncryptedKey: userResp.EncryptedSecretKey,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("密钥解密失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// === 第2步:权限与产品验证 ===
|
||||||
|
|
||||||
|
// 2.1 产品权限检查
|
||||||
|
productResp, err := l.svcCtx.ProductRpc.CheckProductAccess(l.ctx, &product.CheckProductAccessReq{
|
||||||
|
UserId: in.UserId,
|
||||||
|
QueryType: in.QueryType,
|
||||||
|
SecretKey: decryptResp.SecretKey,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("产品权限检查失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2.2 余额检查
|
||||||
|
balanceResp, err := l.svcCtx.BillingRpc.CheckBalance(l.ctx, &billing.CheckBalanceReq{
|
||||||
|
UserId: in.UserId,
|
||||||
|
ProductCode: productResp.ProductCode,
|
||||||
|
QueryType: in.QueryType,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("余额不足: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// === 第3步:执行业务逻辑 ===
|
||||||
|
|
||||||
|
var businessResult *BusinessResult
|
||||||
|
switch in.QueryType {
|
||||||
|
case "risk-assessment":
|
||||||
|
businessResult, err = l.processRiskAssessment(in.Parameters)
|
||||||
|
case "credit-check":
|
||||||
|
businessResult, err = l.processCreditCheck(in.Parameters)
|
||||||
|
case "company-info":
|
||||||
|
businessResult, err = l.processCompanyInfo(in.Parameters)
|
||||||
|
case "data-query":
|
||||||
|
businessResult, err = l.processDataQuery(in.Parameters)
|
||||||
|
default:
|
||||||
|
return nil, errors.New("不支持的查询类型")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("业务处理失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// === 第4步:计费和审计 ===
|
||||||
|
|
||||||
|
// 4.1 执行扣费
|
||||||
|
chargeResp, err := l.svcCtx.BillingRpc.Charge(l.ctx, &billing.ChargeReq{
|
||||||
|
UserId: in.UserId,
|
||||||
|
ProductCode: productResp.ProductCode,
|
||||||
|
Amount: balanceResp.RequiredAmount,
|
||||||
|
TransactionType: in.QueryType,
|
||||||
|
RequestId: generateRequestId(),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("扣费失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4.2 异步记录审计日志
|
||||||
|
go func() {
|
||||||
|
l.svcCtx.AuditRpc.RecordAPICall(context.Background(), &audit.RecordAPICallReq{
|
||||||
|
UserId: in.UserId,
|
||||||
|
EnterpriseId: userResp.EnterpriseId,
|
||||||
|
QueryType: in.QueryType,
|
||||||
|
ClientIp: in.ClientIp,
|
||||||
|
TransactionId: chargeResp.TransactionId,
|
||||||
|
ResponseTime: time.Since(startTime).Milliseconds(),
|
||||||
|
Status: "success",
|
||||||
|
})
|
||||||
|
}()
|
||||||
|
|
||||||
|
return &pb.ProcessDataRequestResp{
|
||||||
|
Success: true,
|
||||||
|
Data: businessResult.ToJSON(),
|
||||||
|
TransactionId: chargeResp.TransactionId,
|
||||||
|
RemainingBalance: chargeResp.RemainingBalance,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🔥 原FLXG逻辑 - 风险评估
|
||||||
|
func (l *DataOrchestratorLogic) processRiskAssessment(params map[string]string) (*BusinessResult, error) {
|
||||||
|
// 调用西部数据源
|
||||||
|
westData, err := l.svcCtx.WestDexClient.QueryRiskData(params)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用百度风控API
|
||||||
|
baiduData, err := l.svcCtx.BaiduClient.RiskAssessment(params)
|
||||||
|
if err != nil {
|
||||||
|
logx.Errorf("百度API调用失败: %v", err)
|
||||||
|
// 降级处理,只使用西部数据
|
||||||
|
}
|
||||||
|
|
||||||
|
// 数据融合处理
|
||||||
|
result := &BusinessResult{
|
||||||
|
Code: "FLXG001",
|
||||||
|
Data: mergeRiskData(westData, baiduData),
|
||||||
|
Source: "west+baidu",
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🔥 原JRZQ逻辑 - 征信查询
|
||||||
|
func (l *DataOrchestratorLogic) processCreditCheck(params map[string]string) (*BusinessResult, error) {
|
||||||
|
// 调用征信API
|
||||||
|
creditData, err := l.svcCtx.CreditClient.QueryCredit(params)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &BusinessResult{
|
||||||
|
Code: "JRZQ001",
|
||||||
|
Data: creditData,
|
||||||
|
Source: "credit_bureau",
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 数据库操作 (go-zero Model)
|
||||||
|
|
||||||
|
#### 用户模型
|
||||||
|
|
||||||
|
```go
|
||||||
|
// rpc/user/internal/model/usermodel.go
|
||||||
|
|
||||||
|
type User struct {
|
||||||
|
Id int64 `db:"id"`
|
||||||
|
Username string `db:"username"`
|
||||||
|
Password string `db:"password"`
|
||||||
|
Email string `db:"email"`
|
||||||
|
EnterpriseId int64 `db:"enterprise_id"`
|
||||||
|
Status int64 `db:"status"`
|
||||||
|
CreatedAt time.Time `db:"created_at"`
|
||||||
|
UpdatedAt time.Time `db:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserModel interface {
|
||||||
|
Insert(ctx context.Context, data *User) (sql.Result, error)
|
||||||
|
FindOne(ctx context.Context, id int64) (*User, error)
|
||||||
|
FindOneByUsername(ctx context.Context, username string) (*User, error)
|
||||||
|
Update(ctx context.Context, data *User) error
|
||||||
|
Delete(ctx context.Context, id int64) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type defaultUserModel struct {
|
||||||
|
conn sqlx.SqlConn
|
||||||
|
table string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUserModel(conn sqlx.SqlConn) UserModel {
|
||||||
|
return &defaultUserModel{
|
||||||
|
conn: conn,
|
||||||
|
table: "`users`",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *defaultUserModel) FindOneByUsername(ctx context.Context, username string) (*User, error) {
|
||||||
|
query := fmt.Sprintf("select %s from %s where `username` = ? limit 1", userRows, m.table)
|
||||||
|
var resp User
|
||||||
|
err := m.conn.QueryRowCtx(ctx, &resp, query, username)
|
||||||
|
switch err {
|
||||||
|
case nil:
|
||||||
|
return &resp, nil
|
||||||
|
case sqlc.ErrNotFound:
|
||||||
|
return nil, ErrNotFound
|
||||||
|
default:
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 缓存处理
|
||||||
|
|
||||||
|
```go
|
||||||
|
// rpc/user/internal/logic/user/getuserinfologic.go
|
||||||
|
|
||||||
|
func (l *GetUserInfoLogic) GetUserInfo(in *pb.GetUserInfoReq) (*pb.GetUserInfoResp, error) {
|
||||||
|
// 1. 先查缓存
|
||||||
|
cacheKey := fmt.Sprintf("user:info:%d", in.UserId)
|
||||||
|
cached, err := l.svcCtx.RedisClient.Get(cacheKey)
|
||||||
|
if err == nil && cached != "" {
|
||||||
|
var userInfo pb.GetUserInfoResp
|
||||||
|
if json.Unmarshal([]byte(cached), &userInfo) == nil {
|
||||||
|
return &userInfo, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 查数据库
|
||||||
|
user, err := l.svcCtx.UserModel.FindOne(l.ctx, in.UserId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
enterprise, err := l.svcCtx.EnterpriseModel.FindOne(l.ctx, user.EnterpriseId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := &pb.GetUserInfoResp{
|
||||||
|
UserId: user.Id,
|
||||||
|
Username: user.Username,
|
||||||
|
Email: user.Email,
|
||||||
|
EnterpriseId: user.EnterpriseId,
|
||||||
|
EnterpriseName: enterprise.Name,
|
||||||
|
EncryptedSecretKey: enterprise.EncryptedSecretKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 写入缓存 (5分钟过期)
|
||||||
|
respJson, _ := json.Marshal(resp)
|
||||||
|
l.svcCtx.RedisClient.Setex(cacheKey, string(respJson), 300)
|
||||||
|
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 部署配置
|
||||||
|
|
||||||
|
### Docker 部署
|
||||||
|
|
||||||
|
#### 1. 服务 Dockerfile
|
||||||
|
|
||||||
|
```dockerfile
|
||||||
|
# rpc/user/Dockerfile
|
||||||
|
FROM golang:1.19-alpine AS builder
|
||||||
|
|
||||||
|
WORKDIR /build
|
||||||
|
COPY . .
|
||||||
|
RUN go mod download
|
||||||
|
RUN go build -ldflags="-s -w" -o user rpc/user/user.go
|
||||||
|
|
||||||
|
FROM alpine:latest
|
||||||
|
RUN apk --no-cache add ca-certificates
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=builder /build/user .
|
||||||
|
COPY --from=builder /build/rpc/user/etc/user.yaml ./etc/
|
||||||
|
|
||||||
|
EXPOSE 8001
|
||||||
|
CMD ["./user", "-f", "etc/user.yaml"]
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. Docker Compose (开发环境)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# deploy/docker/docker-compose.dev.yml
|
||||||
|
version: "3.8"
|
||||||
|
|
||||||
|
services:
|
||||||
|
# 基础设施
|
||||||
|
mysql:
|
||||||
|
image: mysql:8.0
|
||||||
|
environment:
|
||||||
|
MYSQL_ROOT_PASSWORD: root123
|
||||||
|
MYSQL_DATABASE: tianyuan
|
||||||
|
ports:
|
||||||
|
- "3306:3306"
|
||||||
|
volumes:
|
||||||
|
- mysql_data:/var/lib/mysql
|
||||||
|
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis:7-alpine
|
||||||
|
ports:
|
||||||
|
- "6379:6379"
|
||||||
|
|
||||||
|
etcd:
|
||||||
|
image: quay.io/coreos/etcd:v3.5.0
|
||||||
|
environment:
|
||||||
|
- ETCD_ADVERTISE_CLIENT_URLS=http://etcd:2379
|
||||||
|
- ETCD_LISTEN_CLIENT_URLS=http://0.0.0.0:2379
|
||||||
|
ports:
|
||||||
|
- "2379:2379"
|
||||||
|
|
||||||
|
kafka:
|
||||||
|
image: confluentinc/cp-kafka:latest
|
||||||
|
environment:
|
||||||
|
KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
|
||||||
|
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092
|
||||||
|
ports:
|
||||||
|
- "9092:9092"
|
||||||
|
depends_on:
|
||||||
|
- zookeeper
|
||||||
|
|
||||||
|
zookeeper:
|
||||||
|
image: confluentinc/cp-zookeeper:latest
|
||||||
|
environment:
|
||||||
|
ZOOKEEPER_CLIENT_PORT: 2181
|
||||||
|
|
||||||
|
# 微服务
|
||||||
|
user-rpc:
|
||||||
|
build:
|
||||||
|
context: ../../
|
||||||
|
dockerfile: rpc/user/Dockerfile
|
||||||
|
ports:
|
||||||
|
- "8001:8001"
|
||||||
|
environment:
|
||||||
|
- DB_HOST=mysql
|
||||||
|
- REDIS_HOST=redis
|
||||||
|
- ETCD_HOSTS=etcd:2379
|
||||||
|
depends_on:
|
||||||
|
- mysql
|
||||||
|
- redis
|
||||||
|
- etcd
|
||||||
|
|
||||||
|
data-rpc:
|
||||||
|
build:
|
||||||
|
context: ../../
|
||||||
|
dockerfile: rpc/data/Dockerfile
|
||||||
|
ports:
|
||||||
|
- "8002:8002"
|
||||||
|
environment:
|
||||||
|
- DB_HOST=mysql
|
||||||
|
- REDIS_HOST=redis
|
||||||
|
- ETCD_HOSTS=etcd:2379
|
||||||
|
- USER_RPC=user-rpc:8001
|
||||||
|
depends_on:
|
||||||
|
- user-rpc
|
||||||
|
|
||||||
|
gateway-api:
|
||||||
|
build:
|
||||||
|
context: ../../
|
||||||
|
dockerfile: api/gateway/Dockerfile
|
||||||
|
ports:
|
||||||
|
- "8000:8000"
|
||||||
|
environment:
|
||||||
|
- USER_RPC=user-rpc:8001
|
||||||
|
- DATA_RPC=data-rpc:8002
|
||||||
|
- SECURITY_RPC=security-rpc:8003
|
||||||
|
- BILLING_RPC=billing-rpc:8004
|
||||||
|
depends_on:
|
||||||
|
- user-rpc
|
||||||
|
- data-rpc
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
mysql_data:
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. Kubernetes 部署
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# deploy/k8s/user-rpc.yaml
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: user-rpc
|
||||||
|
spec:
|
||||||
|
replicas: 3
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: user-rpc
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: user-rpc
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: user-rpc
|
||||||
|
image: tianyuan/user-rpc:latest
|
||||||
|
ports:
|
||||||
|
- containerPort: 8001
|
||||||
|
env:
|
||||||
|
- name: DB_HOST
|
||||||
|
value: "mysql-svc"
|
||||||
|
- name: REDIS_HOST
|
||||||
|
value: "redis-svc"
|
||||||
|
- name: ETCD_HOSTS
|
||||||
|
value: "etcd-svc:2379"
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
memory: "128Mi"
|
||||||
|
cpu: "100m"
|
||||||
|
limits:
|
||||||
|
memory: "512Mi"
|
||||||
|
cpu: "500m"
|
||||||
|
livenessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /health
|
||||||
|
port: 8001
|
||||||
|
initialDelaySeconds: 30
|
||||||
|
periodSeconds: 10
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /ready
|
||||||
|
port: 8001
|
||||||
|
initialDelaySeconds: 5
|
||||||
|
periodSeconds: 5
|
||||||
|
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: user-rpc-svc
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
app: user-rpc
|
||||||
|
ports:
|
||||||
|
- port: 8001
|
||||||
|
targetPort: 8001
|
||||||
|
type: ClusterIP
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Makefile (统一构建部署)
|
||||||
|
|
||||||
|
```makefile
|
||||||
|
# Makefile
|
||||||
|
.PHONY: build-all start-dev stop-dev deploy-k8s
|
||||||
|
|
||||||
|
# 构建所有服务
|
||||||
|
build-all:
|
||||||
|
@echo "构建所有微服务..."
|
||||||
|
cd api/gateway && go build -o ../../bin/gateway gateway.go
|
||||||
|
cd rpc/user && go build -o ../../bin/user-rpc user.go
|
||||||
|
cd rpc/data && go build -o ../../bin/data-rpc data.go
|
||||||
|
cd rpc/security && go build -o ../../bin/security-rpc security.go
|
||||||
|
cd rpc/billing && go build -o ../../bin/billing-rpc billing.go
|
||||||
|
|
||||||
|
# 生成代码
|
||||||
|
gen-api:
|
||||||
|
cd api/gateway && goctl api go -api gateway.api -dir .
|
||||||
|
|
||||||
|
gen-rpc:
|
||||||
|
cd rpc/user && goctl rpc protoc user.proto --go_out=. --go-grpc_out=. --zrpc_out=.
|
||||||
|
|
||||||
|
# 开发环境
|
||||||
|
start-dev:
|
||||||
|
docker-compose -f deploy/docker/docker-compose.dev.yml up -d
|
||||||
|
|
||||||
|
stop-dev:
|
||||||
|
docker-compose -f deploy/docker/docker-compose.dev.yml down
|
||||||
|
|
||||||
|
# 数据库迁移
|
||||||
|
migrate-up:
|
||||||
|
cd tools/migrate && go run migrate.go up
|
||||||
|
|
||||||
|
migrate-down:
|
||||||
|
cd tools/migrate && go run migrate.go down
|
||||||
|
|
||||||
|
# K8s部署
|
||||||
|
deploy-k8s:
|
||||||
|
kubectl apply -f deploy/k8s/namespace.yaml
|
||||||
|
kubectl apply -f deploy/k8s/configmap.yaml
|
||||||
|
kubectl apply -f deploy/k8s/mysql.yaml
|
||||||
|
kubectl apply -f deploy/k8s/redis.yaml
|
||||||
|
kubectl apply -f deploy/k8s/user-rpc.yaml
|
||||||
|
kubectl apply -f deploy/k8s/data-rpc.yaml
|
||||||
|
kubectl apply -f deploy/k8s/gateway-api.yaml
|
||||||
|
|
||||||
|
# 测试
|
||||||
|
test-all:
|
||||||
|
go test ./api/gateway/...
|
||||||
|
go test ./rpc/user/...
|
||||||
|
go test ./rpc/data/...
|
||||||
|
|
||||||
|
# 代码检查
|
||||||
|
lint:
|
||||||
|
golangci-lint run ./...
|
||||||
|
|
||||||
|
# 清理
|
||||||
|
clean:
|
||||||
|
rm -rf bin/
|
||||||
|
docker system prune -f
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔄 CI/CD 配置
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# .github/workflows/deploy.yml
|
||||||
|
name: Deploy Microservices
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v3
|
||||||
|
with:
|
||||||
|
go-version: 1.19
|
||||||
|
|
||||||
|
- name: Build services
|
||||||
|
run: make build-all
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: make test-all
|
||||||
|
|
||||||
|
- name: Build Docker images
|
||||||
|
run: |
|
||||||
|
docker build -t tianyuan/gateway:${{ github.sha }} api/gateway/
|
||||||
|
docker build -t tianyuan/user-rpc:${{ github.sha }} rpc/user/
|
||||||
|
docker build -t tianyuan/data-rpc:${{ github.sha }} rpc/data/
|
||||||
|
|
||||||
|
- name: Push to registry
|
||||||
|
run: |
|
||||||
|
echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin
|
||||||
|
docker push tianyuan/gateway:${{ github.sha }}
|
||||||
|
docker push tianyuan/user-rpc:${{ github.sha }}
|
||||||
|
docker push tianyuan/data-rpc:${{ github.sha }}
|
||||||
|
|
||||||
|
- name: Deploy to K8s
|
||||||
|
run: |
|
||||||
|
echo "${{ secrets.KUBECONFIG }}" | base64 -d > kubeconfig
|
||||||
|
export KUBECONFIG=kubeconfig
|
||||||
|
sed -i 's|latest|${{ github.sha }}|g' deploy/k8s/*.yaml
|
||||||
|
kubectl apply -f deploy/k8s/
|
||||||
|
```
|
||||||
|
|
||||||
|
这个架构设计完全基于 go-zero 框架,保持了你原有业务逻辑的同时,提供了清晰的服务边界和强大的扩展能力。每个服务都可以独立开发、测试、部署和扩容。
|
||||||
739
go-zero错误分级与链路追踪设计.md
Normal file
739
go-zero错误分级与链路追踪设计.md
Normal file
@@ -0,0 +1,739 @@
|
|||||||
|
# go-zero 错误分级与链路追踪设计
|
||||||
|
|
||||||
|
## 1. 错误分级体系
|
||||||
|
|
||||||
|
### 1.1 错误等级定义
|
||||||
|
|
||||||
|
```go
|
||||||
|
// shared/errcode/levels.go
|
||||||
|
package errcode
|
||||||
|
|
||||||
|
// 错误等级枚举
|
||||||
|
type ErrorLevel int
|
||||||
|
|
||||||
|
const (
|
||||||
|
LevelDebug ErrorLevel = iota // 调试级别:开发调试信息
|
||||||
|
LevelInfo // 信息级别:一般业务信息
|
||||||
|
LevelWarn // 警告级别:需要关注但不影响业务
|
||||||
|
LevelError // 错误级别:业务错误,需要处理
|
||||||
|
LevelFatal // 致命级别:系统级错误,影响服务
|
||||||
|
LevelPanic // 恐慌级别:严重错误,服务不可用
|
||||||
|
)
|
||||||
|
|
||||||
|
// 错误等级字符串映射
|
||||||
|
var LevelNames = map[ErrorLevel]string{
|
||||||
|
LevelDebug: "DEBUG",
|
||||||
|
LevelInfo: "INFO",
|
||||||
|
LevelWarn: "WARN",
|
||||||
|
LevelError: "ERROR",
|
||||||
|
LevelFatal: "FATAL",
|
||||||
|
LevelPanic: "PANIC",
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l ErrorLevel) String() string {
|
||||||
|
if name, ok := LevelNames[l]; ok {
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
return "UNKNOWN"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.2 错误分类体系
|
||||||
|
|
||||||
|
```go
|
||||||
|
// shared/errcode/types.go
|
||||||
|
package errcode
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 错误类型
|
||||||
|
type ErrorType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// 系统级错误
|
||||||
|
ErrorTypeSystem ErrorType = "SYSTEM" // 系统错误
|
||||||
|
ErrorTypeNetwork ErrorType = "NETWORK" // 网络错误
|
||||||
|
ErrorTypeDatabase ErrorType = "DATABASE" // 数据库错误
|
||||||
|
ErrorTypeRedis ErrorType = "REDIS" // Redis错误
|
||||||
|
ErrorTypeMQ ErrorType = "MQ" // 消息队列错误
|
||||||
|
ErrorTypeRPC ErrorType = "RPC" // RPC调用错误
|
||||||
|
|
||||||
|
// 业务级错误
|
||||||
|
ErrorTypeBusiness ErrorType = "BUSINESS" // 业务逻辑错误
|
||||||
|
ErrorTypeValidation ErrorType = "VALIDATION" // 参数校验错误
|
||||||
|
ErrorTypeAuth ErrorType = "AUTH" // 认证授权错误
|
||||||
|
ErrorTypePermission ErrorType = "PERMISSION" // 权限错误
|
||||||
|
|
||||||
|
// 客户端错误
|
||||||
|
ErrorTypeParam ErrorType = "PARAM" // 参数错误
|
||||||
|
ErrorTypeRequest ErrorType = "REQUEST" // 请求错误
|
||||||
|
ErrorTypeResponse ErrorType = "RESPONSE" // 响应错误
|
||||||
|
)
|
||||||
|
|
||||||
|
// 统一错误结构
|
||||||
|
type AppError struct {
|
||||||
|
Code string `json:"code"` // 错误码
|
||||||
|
Message string `json:"message"` // 错误消息
|
||||||
|
Level ErrorLevel `json:"level"` // 错误等级
|
||||||
|
Type ErrorType `json:"type"` // 错误类型
|
||||||
|
TraceId string `json:"trace_id"` // 链路追踪ID
|
||||||
|
SpanId string `json:"span_id"` // 跨度ID
|
||||||
|
Service string `json:"service"` // 服务名称
|
||||||
|
Method string `json:"method"` // 方法名称
|
||||||
|
Timestamp time.Time `json:"timestamp"` // 时间戳
|
||||||
|
Details interface{} `json:"details"` // 详细信息
|
||||||
|
Stack string `json:"stack"` // 堆栈信息(仅错误级别以上)
|
||||||
|
Cause error `json:"-"` // 原始错误(不序列化)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 实现error接口
|
||||||
|
func (e *AppError) Error() string {
|
||||||
|
return fmt.Sprintf("[%s][%s][%s] %s: %s",
|
||||||
|
e.Level.String(), e.Type, e.Code, e.Service, e.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取原始错误
|
||||||
|
func (e *AppError) Unwrap() error {
|
||||||
|
return e.Cause
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.3 错误构造器
|
||||||
|
|
||||||
|
```go
|
||||||
|
// shared/errcode/builder.go
|
||||||
|
package errcode
|
||||||
|
|
||||||
|
import (
|
||||||
|
"runtime"
|
||||||
|
"time"
|
||||||
|
"github.com/zeromicro/go-zero/core/trace"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ErrorBuilder struct {
|
||||||
|
service string
|
||||||
|
method string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewErrorBuilder(service, method string) *ErrorBuilder {
|
||||||
|
return &ErrorBuilder{
|
||||||
|
service: service,
|
||||||
|
method: method,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debug级别错误
|
||||||
|
func (b *ErrorBuilder) Debug(code, message string) *AppError {
|
||||||
|
return b.buildError(LevelDebug, ErrorTypeSystem, code, message, nil, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Info级别错误
|
||||||
|
func (b *ErrorBuilder) Info(code, message string) *AppError {
|
||||||
|
return b.buildError(LevelInfo, ErrorTypeSystem, code, message, nil, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warn级别错误
|
||||||
|
func (b *ErrorBuilder) Warn(errorType ErrorType, code, message string) *AppError {
|
||||||
|
return b.buildError(LevelWarn, errorType, code, message, nil, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error级别错误
|
||||||
|
func (b *ErrorBuilder) Error(errorType ErrorType, code, message string, cause error) *AppError {
|
||||||
|
return b.buildError(LevelError, errorType, code, message, cause, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fatal级别错误
|
||||||
|
func (b *ErrorBuilder) Fatal(errorType ErrorType, code, message string, cause error) *AppError {
|
||||||
|
return b.buildError(LevelFatal, errorType, code, message, cause, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Panic级别错误
|
||||||
|
func (b *ErrorBuilder) Panic(errorType ErrorType, code, message string, cause error) *AppError {
|
||||||
|
return b.buildError(LevelPanic, errorType, code, message, cause, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 业务错误(常用)
|
||||||
|
func (b *ErrorBuilder) BusinessError(code, message string) *AppError {
|
||||||
|
return b.buildError(LevelError, ErrorTypeBusiness, code, message, nil, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 参数校验错误(常用)
|
||||||
|
func (b *ErrorBuilder) ValidationError(code, message string, details interface{}) *AppError {
|
||||||
|
return b.buildError(LevelWarn, ErrorTypeValidation, code, message, nil, details)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 权限错误(常用)
|
||||||
|
func (b *ErrorBuilder) PermissionError(code, message string) *AppError {
|
||||||
|
return b.buildError(LevelWarn, ErrorTypePermission, code, message, nil, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 系统错误(常用)
|
||||||
|
func (b *ErrorBuilder) SystemError(code, message string, cause error) *AppError {
|
||||||
|
return b.buildError(LevelFatal, ErrorTypeSystem, code, message, cause, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建错误
|
||||||
|
func (b *ErrorBuilder) buildError(level ErrorLevel, errorType ErrorType, code, message string, cause error, details interface{}) *AppError {
|
||||||
|
appErr := &AppError{
|
||||||
|
Code: code,
|
||||||
|
Message: message,
|
||||||
|
Level: level,
|
||||||
|
Type: errorType,
|
||||||
|
Service: b.service,
|
||||||
|
Method: b.method,
|
||||||
|
Timestamp: time.Now(),
|
||||||
|
Details: details,
|
||||||
|
Cause: cause,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取链路追踪信息
|
||||||
|
if traceId := trace.TraceIDFromContext(ctx); traceId != "" {
|
||||||
|
appErr.TraceId = traceId
|
||||||
|
}
|
||||||
|
if spanId := trace.SpanIDFromContext(ctx); spanId != "" {
|
||||||
|
appErr.SpanId = spanId
|
||||||
|
}
|
||||||
|
|
||||||
|
// 错误级别以上记录堆栈信息
|
||||||
|
if level >= LevelError {
|
||||||
|
appErr.Stack = getStackTrace()
|
||||||
|
}
|
||||||
|
|
||||||
|
return appErr
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取堆栈信息
|
||||||
|
func getStackTrace() string {
|
||||||
|
buf := make([]byte, 4096)
|
||||||
|
n := runtime.Stack(buf, false)
|
||||||
|
return string(buf[:n])
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. 链路追踪集成
|
||||||
|
|
||||||
|
### 2.1 链路追踪配置
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# etc/client-api.yaml
|
||||||
|
Name: client-api
|
||||||
|
Host: 0.0.0.0
|
||||||
|
Port: 8080
|
||||||
|
|
||||||
|
# 链路追踪配置
|
||||||
|
Telemetry:
|
||||||
|
Name: client-api
|
||||||
|
Endpoint: http://jaeger:14268/api/traces
|
||||||
|
Sampler: 1.0
|
||||||
|
Batcher: jaeger
|
||||||
|
|
||||||
|
# 日志配置
|
||||||
|
Log:
|
||||||
|
ServiceName: client-api
|
||||||
|
Mode: file
|
||||||
|
Level: info
|
||||||
|
Path: logs
|
||||||
|
MaxSize: 100
|
||||||
|
MaxAge: 7
|
||||||
|
MaxBackups: 5
|
||||||
|
Compress: true
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 链路追踪中间件
|
||||||
|
|
||||||
|
```go
|
||||||
|
// shared/middleware/trace_middleware.go
|
||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"github.com/zeromicro/go-zero/core/trace"
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
|
"go.opentelemetry.io/otel"
|
||||||
|
"go.opentelemetry.io/otel/attribute"
|
||||||
|
"go.opentelemetry.io/otel/codes"
|
||||||
|
"tianyuan/shared/errcode"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HTTP链路追踪中间件
|
||||||
|
func TraceMiddleware(serviceName string) func(http.Handler) http.Handler {
|
||||||
|
return func(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
tracer := otel.Tracer(serviceName)
|
||||||
|
|
||||||
|
// 开始span
|
||||||
|
ctx, span := tracer.Start(r.Context(), r.URL.Path)
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
|
// 设置span属性
|
||||||
|
span.SetAttributes(
|
||||||
|
attribute.String("http.method", r.Method),
|
||||||
|
attribute.String("http.url", r.URL.String()),
|
||||||
|
attribute.String("http.user_agent", r.UserAgent()),
|
||||||
|
attribute.String("service.name", serviceName),
|
||||||
|
)
|
||||||
|
|
||||||
|
// 将链路信息注入上下文
|
||||||
|
r = r.WithContext(ctx)
|
||||||
|
|
||||||
|
// 创建响应包装器
|
||||||
|
wrapper := &responseWrapper{
|
||||||
|
ResponseWriter: w,
|
||||||
|
statusCode: http.StatusOK,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行下一个处理器
|
||||||
|
next.ServeHTTP(wrapper, r)
|
||||||
|
|
||||||
|
// 设置响应属性
|
||||||
|
span.SetAttributes(
|
||||||
|
attribute.Int("http.status_code", wrapper.statusCode),
|
||||||
|
)
|
||||||
|
|
||||||
|
// 如果是错误状态码,设置span状态
|
||||||
|
if wrapper.statusCode >= 400 {
|
||||||
|
span.SetStatus(codes.Error, http.StatusText(wrapper.statusCode))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 响应包装器
|
||||||
|
type responseWrapper struct {
|
||||||
|
http.ResponseWriter
|
||||||
|
statusCode int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *responseWrapper) WriteHeader(statusCode int) {
|
||||||
|
w.statusCode = statusCode
|
||||||
|
w.ResponseWriter.WriteHeader(statusCode)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.3 RPC 链路追踪拦截器
|
||||||
|
|
||||||
|
```go
|
||||||
|
// shared/interceptor/trace_interceptor.go
|
||||||
|
package interceptor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
"go.opentelemetry.io/otel"
|
||||||
|
"go.opentelemetry.io/otel/attribute"
|
||||||
|
"go.opentelemetry.io/otel/codes"
|
||||||
|
"tianyuan/shared/errcode"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RPC客户端链路追踪拦截器
|
||||||
|
func TraceClientInterceptor(serviceName string) grpc.UnaryClientInterceptor {
|
||||||
|
return func(ctx context.Context, method string, req, reply interface{},
|
||||||
|
cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
|
||||||
|
|
||||||
|
tracer := otel.Tracer(serviceName)
|
||||||
|
ctx, span := tracer.Start(ctx, method)
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
|
// 设置span属性
|
||||||
|
span.SetAttributes(
|
||||||
|
attribute.String("rpc.method", method),
|
||||||
|
attribute.String("rpc.service", serviceName),
|
||||||
|
attribute.String("rpc.system", "grpc"),
|
||||||
|
)
|
||||||
|
|
||||||
|
// 调用RPC
|
||||||
|
err := invoker(ctx, method, req, reply, cc, opts...)
|
||||||
|
|
||||||
|
// 处理错误
|
||||||
|
if err != nil {
|
||||||
|
span.SetStatus(codes.Error, err.Error())
|
||||||
|
span.SetAttributes(
|
||||||
|
attribute.String("rpc.grpc.status_code", status.Code(err).String()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RPC服务端链路追踪拦截器
|
||||||
|
func TraceServerInterceptor(serviceName string) grpc.UnaryServerInterceptor {
|
||||||
|
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo,
|
||||||
|
handler grpc.UnaryHandler) (interface{}, error) {
|
||||||
|
|
||||||
|
tracer := otel.Tracer(serviceName)
|
||||||
|
ctx, span := tracer.Start(ctx, info.FullMethod)
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
|
// 设置span属性
|
||||||
|
span.SetAttributes(
|
||||||
|
attribute.String("rpc.method", info.FullMethod),
|
||||||
|
attribute.String("rpc.service", serviceName),
|
||||||
|
attribute.String("rpc.system", "grpc"),
|
||||||
|
)
|
||||||
|
|
||||||
|
// 调用处理器
|
||||||
|
resp, err := handler(ctx, req)
|
||||||
|
|
||||||
|
// 处理错误
|
||||||
|
if err != nil {
|
||||||
|
span.SetStatus(codes.Error, err.Error())
|
||||||
|
|
||||||
|
// 如果是自定义错误,记录更多信息
|
||||||
|
if appErr, ok := err.(*errcode.AppError); ok {
|
||||||
|
span.SetAttributes(
|
||||||
|
attribute.String("error.type", string(appErr.Type)),
|
||||||
|
attribute.String("error.code", appErr.Code),
|
||||||
|
attribute.String("error.level", appErr.Level.String()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. 日志集成
|
||||||
|
|
||||||
|
### 3.1 结构化日志
|
||||||
|
|
||||||
|
```go
|
||||||
|
// shared/logger/logger.go
|
||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"github.com/zeromicro/go-zero/core/logx"
|
||||||
|
"github.com/zeromicro/go-zero/core/trace"
|
||||||
|
"tianyuan/shared/errcode"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 日志字段
|
||||||
|
type LogFields map[string]interface{}
|
||||||
|
|
||||||
|
// 结构化日志器
|
||||||
|
type StructuredLogger struct {
|
||||||
|
service string
|
||||||
|
method string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewStructuredLogger(service, method string) *StructuredLogger {
|
||||||
|
return &StructuredLogger{
|
||||||
|
service: service,
|
||||||
|
method: method,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 记录错误日志
|
||||||
|
func (l *StructuredLogger) LogError(ctx context.Context, err error, fields LogFields) {
|
||||||
|
logFields := l.buildBaseFields(ctx, fields)
|
||||||
|
|
||||||
|
if appErr, ok := err.(*errcode.AppError); ok {
|
||||||
|
// 自定义错误
|
||||||
|
logFields["error_code"] = appErr.Code
|
||||||
|
logFields["error_type"] = appErr.Type
|
||||||
|
logFields["error_level"] = appErr.Level.String()
|
||||||
|
logFields["error_details"] = appErr.Details
|
||||||
|
|
||||||
|
// 根据错误级别选择日志方法
|
||||||
|
switch appErr.Level {
|
||||||
|
case errcode.LevelDebug:
|
||||||
|
logx.WithContext(ctx).WithFields(logFields).Info(appErr.Message)
|
||||||
|
case errcode.LevelInfo:
|
||||||
|
logx.WithContext(ctx).WithFields(logFields).Info(appErr.Message)
|
||||||
|
case errcode.LevelWarn:
|
||||||
|
logx.WithContext(ctx).WithFields(logFields).Slow(appErr.Message)
|
||||||
|
case errcode.LevelError:
|
||||||
|
logx.WithContext(ctx).WithFields(logFields).Error(appErr.Message)
|
||||||
|
case errcode.LevelFatal, errcode.LevelPanic:
|
||||||
|
logx.WithContext(ctx).WithFields(logFields).Severe(appErr.Message)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 普通错误
|
||||||
|
logFields["error"] = err.Error()
|
||||||
|
logx.WithContext(ctx).WithFields(logFields).Error(err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 记录业务日志
|
||||||
|
func (l *StructuredLogger) LogInfo(ctx context.Context, message string, fields LogFields) {
|
||||||
|
logFields := l.buildBaseFields(ctx, fields)
|
||||||
|
logx.WithContext(ctx).WithFields(logFields).Info(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 记录警告日志
|
||||||
|
func (l *StructuredLogger) LogWarn(ctx context.Context, message string, fields LogFields) {
|
||||||
|
logFields := l.buildBaseFields(ctx, fields)
|
||||||
|
logx.WithContext(ctx).WithFields(logFields).Slow(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建基础日志字段
|
||||||
|
func (l *StructuredLogger) buildBaseFields(ctx context.Context, fields LogFields) logx.LogField {
|
||||||
|
baseFields := logx.LogField{
|
||||||
|
"service": l.service,
|
||||||
|
"method": l.method,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加链路追踪信息
|
||||||
|
if traceId := trace.TraceIDFromContext(ctx); traceId != "" {
|
||||||
|
baseFields["trace_id"] = traceId
|
||||||
|
}
|
||||||
|
if spanId := trace.SpanIDFromContext(ctx); spanId != "" {
|
||||||
|
baseFields["span_id"] = spanId
|
||||||
|
}
|
||||||
|
|
||||||
|
// 合并自定义字段
|
||||||
|
for k, v := range fields {
|
||||||
|
baseFields[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
return baseFields
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 日志中间件
|
||||||
|
|
||||||
|
```go
|
||||||
|
// shared/middleware/log_middleware.go
|
||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
|
"tianyuan/shared/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HTTP日志中间件
|
||||||
|
func LogMiddleware(serviceName string) func(http.Handler) http.Handler {
|
||||||
|
return func(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
start := time.Now()
|
||||||
|
|
||||||
|
logger := logger.NewStructuredLogger(serviceName, r.URL.Path)
|
||||||
|
|
||||||
|
// 记录请求开始
|
||||||
|
logger.LogInfo(r.Context(), "request_start", logger.LogFields{
|
||||||
|
"method": r.Method,
|
||||||
|
"path": r.URL.Path,
|
||||||
|
"query": r.URL.RawQuery,
|
||||||
|
"user_agent": r.UserAgent(),
|
||||||
|
"remote_ip": httpx.GetRemoteAddr(r),
|
||||||
|
})
|
||||||
|
|
||||||
|
// 创建响应包装器
|
||||||
|
wrapper := &responseWrapper{
|
||||||
|
ResponseWriter: w,
|
||||||
|
statusCode: http.StatusOK,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行请求
|
||||||
|
next.ServeHTTP(wrapper, r)
|
||||||
|
|
||||||
|
// 记录请求结束
|
||||||
|
duration := time.Since(start)
|
||||||
|
fields := logger.LogFields{
|
||||||
|
"status_code": wrapper.statusCode,
|
||||||
|
"duration_ms": duration.Milliseconds(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if wrapper.statusCode >= 400 {
|
||||||
|
logger.LogWarn(r.Context(), "request_error", fields)
|
||||||
|
} else {
|
||||||
|
logger.LogInfo(r.Context(), "request_success", fields)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. 使用示例
|
||||||
|
|
||||||
|
### 4.1 在 Handler 中使用
|
||||||
|
|
||||||
|
```go
|
||||||
|
// client/internal/handler/product/getproductlisthandler.go
|
||||||
|
func (h *GetProductListHandler) GetProductList(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// 创建错误构造器
|
||||||
|
errBuilder := errcode.NewErrorBuilder("client-api", "GetProductList")
|
||||||
|
logger := logger.NewStructuredLogger("client-api", "GetProductList")
|
||||||
|
|
||||||
|
var req types.GetProductListReq
|
||||||
|
|
||||||
|
// 参数校验
|
||||||
|
if err := validator.ValidateAndParse(r, &req); err != nil {
|
||||||
|
appErr := errBuilder.ValidationError("PARAM_INVALID", "参数校验失败", err)
|
||||||
|
logger.LogError(r.Context(), appErr, logger.LogFields{
|
||||||
|
"request": req,
|
||||||
|
})
|
||||||
|
response.ErrorResponse(w, appErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用Logic层
|
||||||
|
resp, err := h.logic.GetProductList(r.Context(), &req)
|
||||||
|
if err != nil {
|
||||||
|
logger.LogError(r.Context(), err, logger.LogFields{
|
||||||
|
"request": req,
|
||||||
|
})
|
||||||
|
response.ErrorResponse(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 记录成功日志
|
||||||
|
logger.LogInfo(r.Context(), "get_product_list_success", logger.LogFields{
|
||||||
|
"request": req,
|
||||||
|
"result_count": len(resp.List),
|
||||||
|
})
|
||||||
|
|
||||||
|
response.SuccessResponse(w, resp)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 在 RPC Logic 中使用
|
||||||
|
|
||||||
|
```go
|
||||||
|
// domains/product/rpc/internal/logic/getproductlistlogic.go
|
||||||
|
func (l *GetProductListLogic) GetProductList(ctx context.Context, req *product.GetProductListReq) (*product.GetProductListResp, error) {
|
||||||
|
errBuilder := errcode.NewErrorBuilder("product-rpc", "GetProductList")
|
||||||
|
logger := logger.NewStructuredLogger("product-rpc", "GetProductList")
|
||||||
|
|
||||||
|
// 业务校验
|
||||||
|
validator := validator.NewProductValidator(ctx, l.svcCtx)
|
||||||
|
if err := validator.ValidateGetProductListRequest(req); err != nil {
|
||||||
|
appErr := errBuilder.BusinessError("VALIDATION_FAILED", err.Error())
|
||||||
|
logger.LogError(ctx, appErr, logger.LogFields{
|
||||||
|
"request": req,
|
||||||
|
})
|
||||||
|
return nil, appErr
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询数据库
|
||||||
|
products, err := l.svcCtx.ProductModel.FindList(ctx, req)
|
||||||
|
if err != nil {
|
||||||
|
appErr := errBuilder.SystemError("DB_QUERY_FAILED", "查询产品列表失败", err)
|
||||||
|
logger.LogError(ctx, appErr, logger.LogFields{
|
||||||
|
"request": req,
|
||||||
|
"db_error": err.Error(),
|
||||||
|
})
|
||||||
|
return nil, appErr
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.LogInfo(ctx, "get_product_list_success", logger.LogFields{
|
||||||
|
"request": req,
|
||||||
|
"result_count": len(products),
|
||||||
|
})
|
||||||
|
|
||||||
|
return &product.GetProductListResp{
|
||||||
|
List: products,
|
||||||
|
Total: int64(len(products)),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. 监控和告警
|
||||||
|
|
||||||
|
### 5.1 错误监控配置
|
||||||
|
|
||||||
|
```go
|
||||||
|
// shared/monitor/error_monitor.go
|
||||||
|
package monitor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"github.com/zeromicro/go-zero/core/metric"
|
||||||
|
"tianyuan/shared/errcode"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// 错误计数器
|
||||||
|
ErrorCounter = metric.NewCounterVec(&metric.CounterVecOpts{
|
||||||
|
Namespace: "tianyuan",
|
||||||
|
Subsystem: "error",
|
||||||
|
Name: "total",
|
||||||
|
Help: "Total number of errors",
|
||||||
|
Labels: []string{"service", "type", "level", "code"},
|
||||||
|
})
|
||||||
|
|
||||||
|
// 错误率直方图
|
||||||
|
ErrorRateHistogram = metric.NewHistogramVec(&metric.HistogramVecOpts{
|
||||||
|
Namespace: "tianyuan",
|
||||||
|
Subsystem: "error",
|
||||||
|
Name: "rate",
|
||||||
|
Help: "Error rate histogram",
|
||||||
|
Labels: []string{"service", "method"},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
// 记录错误指标
|
||||||
|
func RecordError(appErr *errcode.AppError) {
|
||||||
|
ErrorCounter.Inc(
|
||||||
|
appErr.Service,
|
||||||
|
string(appErr.Type),
|
||||||
|
appErr.Level.String(),
|
||||||
|
appErr.Code,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 告警规则
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# prometheus告警规则
|
||||||
|
groups:
|
||||||
|
- name: tianyuan-errors
|
||||||
|
rules:
|
||||||
|
# 错误率告警
|
||||||
|
- alert: HighErrorRate
|
||||||
|
expr: rate(tianyuan_error_total[5m]) > 0.1
|
||||||
|
for: 2m
|
||||||
|
labels:
|
||||||
|
severity: warning
|
||||||
|
annotations:
|
||||||
|
summary: "High error rate detected"
|
||||||
|
description: "Error rate is {{ $value }} for service {{ $labels.service }}"
|
||||||
|
|
||||||
|
# 致命错误告警
|
||||||
|
- alert: FatalError
|
||||||
|
expr: increase(tianyuan_error_total{level="FATAL"}[1m]) > 0
|
||||||
|
for: 0m
|
||||||
|
labels:
|
||||||
|
severity: critical
|
||||||
|
annotations:
|
||||||
|
summary: "Fatal error detected"
|
||||||
|
description: "Fatal error in service {{ $labels.service }}: {{ $labels.code }}"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. 最佳实践总结
|
||||||
|
|
||||||
|
1. **错误分级原则**:
|
||||||
|
|
||||||
|
- DEBUG/INFO:开发调试信息
|
||||||
|
- WARN:需要关注但不影响业务
|
||||||
|
- ERROR:业务错误,需要处理
|
||||||
|
- FATAL/PANIC:系统级错误,需要立即处理
|
||||||
|
|
||||||
|
2. **链路追踪要点**:
|
||||||
|
|
||||||
|
- 每个请求都有唯一的 TraceId
|
||||||
|
- 跨服务调用保持链路连续性
|
||||||
|
- 关键操作添加自定义 Span
|
||||||
|
|
||||||
|
3. **日志记录规范**:
|
||||||
|
|
||||||
|
- 结构化日志,便于查询分析
|
||||||
|
- 包含链路追踪信息
|
||||||
|
- 敏感信息脱敏处理
|
||||||
|
|
||||||
|
4. **监控告警策略**:
|
||||||
|
- 错误率监控
|
||||||
|
- 关键错误实时告警
|
||||||
|
- 链路追踪性能监控
|
||||||
331
par.json
Normal file
331
par.json
Normal file
@@ -0,0 +1,331 @@
|
|||||||
|
{
|
||||||
|
sxbzxr: [
|
||||||
|
{ msg: '没有找到', sxbzxr: '[]'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
entout: [
|
||||||
|
{
|
||||||
|
msg: '查询成功',
|
||||||
|
entout: '{
|
||||||
|
"preservation": {},
|
||||||
|
"crc": 1635002023,
|
||||||
|
"cases_tree": {
|
||||||
|
"criminal": [
|
||||||
|
{
|
||||||
|
"stage_type": 1,
|
||||||
|
"n_ajbs": "xxxxxxxxxxxxxxxxxxxx",
|
||||||
|
"case_type": 200,
|
||||||
|
"c_ah": "(20XX)X省XX刑初XX号"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"stage_type": 2,
|
||||||
|
"n_ajbs": "xxxxxxxxxxxxxxxxxxxx",
|
||||||
|
"case_type": 200,
|
||||||
|
"c_ah": "(20XX)X省XX刑终XX号"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"stage_type": 1,
|
||||||
|
"n_ajbs": "xxxxxxxxxxxxxxxxxxxx",
|
||||||
|
"case_type": 200,
|
||||||
|
"c_ah": "(20XX)X省XX刑初XX号"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"stage_type": 2,
|
||||||
|
"n_ajbs": "xxxxxxxxxxxxxxxxxxxx",
|
||||||
|
"case_type": 200,
|
||||||
|
"c_ah": "(20XX)X省XX刑终XX号"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"administrative": {},
|
||||||
|
"civil": {},
|
||||||
|
"count": {
|
||||||
|
"money_jie_total": 11,
|
||||||
|
"count_total": 4,
|
||||||
|
"larq_stat": "20XX(2),20XX(2)",
|
||||||
|
"money_wei_percent": 0,
|
||||||
|
"area_stat": "XX省(4)",
|
||||||
|
"money_jie_beigao": 0,
|
||||||
|
"count_jie_total": 4,
|
||||||
|
"money_jie_other": 0,
|
||||||
|
"count_wei_total": 0,
|
||||||
|
"count_jie_beigao": 2,
|
||||||
|
"money_yuangao": 11,
|
||||||
|
"money_beigao": 0,
|
||||||
|
"ay_stat": "妨害社会管理秩序罪(4)",
|
||||||
|
"count_wei_other": 0,
|
||||||
|
"count_wei_beigao": 0,
|
||||||
|
"count_wei_yuangao": 0,
|
||||||
|
"money_other": 0,
|
||||||
|
"count_yuangao": 2,
|
||||||
|
"money_wei_yuangao": 0,
|
||||||
|
"money_jie_yuangao": 11,
|
||||||
|
"money_wei_beigao": 0,
|
||||||
|
"count_jie_yuangao": 2,
|
||||||
|
"count_other": 0,
|
||||||
|
"count_jie_other": 0,
|
||||||
|
"count_beigao": 2,
|
||||||
|
"money_wei_total": 0,
|
||||||
|
"money_wei_other": 0,
|
||||||
|
"money_total": 11,
|
||||||
|
"jafs_stat": "判决(2),维持(1),改判(1)"
|
||||||
|
},
|
||||||
|
"implement": {},
|
||||||
|
"criminal": {
|
||||||
|
"cases": [
|
||||||
|
{
|
||||||
|
"n_laay_tree": "妨害社会管理秩序罪,扰乱公共秩序罪,开设赌场罪",
|
||||||
|
"n_jaay": "妨害社会管理秩序罪",
|
||||||
|
"n_ssdw": "被告人",
|
||||||
|
"n_jaay_tree": "妨害社会管理秩 序罪,扰乱公共秩序罪,开设赌场罪",
|
||||||
|
"n_ccxzxje_level": 0,
|
||||||
|
"n_ssdw_ys": "被告人",
|
||||||
|
"c_gkws_dsr": "公诉机关XX省XX县人民检察院。被告人张某,男,XXXX年X月XX日出生于XX省XX县,XX族,小学文化,农民,住XX省XX县。因涉嫌犯开设赌场罪于XXXX年X月XX日被刑事拘留,同年XX月XX日被逮捕。被告人李某,男,XXXX年X月XX日出生于XX省XX县,XX族,小学文化,农民,住XX省XX县 。因涉嫌犯开设赌场罪于XXXX年X月XX日被刑事拘留,同年XX月XX日被逮捕。被告人王某,女,XXXX年X月XX日出生于XX省XX县,XX族,初中文化,农民,住XX省XX县。因涉嫌犯开设赌场罪于XXXX年XX月X日被羁押,次日被刑事拘留,同月XX日被逮捕。被告人赵某,男,XXXX年X月XX日出生于XX省XX县,XX族,小学文化,农民,住XX省XX县。因涉嫌犯开设赌场罪于XXXX年X月XX日被刑事拘留,同年XX月XX日被逮捕。被告人刘某,男,XXXX年X月X日出生于XX省XX县,XX族,初中文化,农民,住XX省XX县。因涉嫌犯开设赌场罪于XXXX年XX月XX日被刑事拘留,同年XX月XX日被逮捕。被告人陈某,男,XXXX年XX月X日出生于XX省XX县,XX族,初中文化,农民,住XX省XX县。因涉嫌犯开设赌场罪于XXXX年XX月XX日被刑事拘留,同年XX月XX日被逮捕。",
|
||||||
|
"n_jafs": "判决",
|
||||||
|
"n_jbfy_cj": "基层法院",
|
||||||
|
"c_id": "xxxxxxxxxxxxxxxxxxxx",
|
||||||
|
"n_slcx": "一审",
|
||||||
|
"n_ajjzjd": "已结案",
|
||||||
|
"n_jbfy": "XX县人民法院",
|
||||||
|
"c_gkws_id": "xxxxxxxxxxxxxxxxxxxx",
|
||||||
|
"d_jarq": "20XX-XX-XX",
|
||||||
|
"c_slfsxx": "1,20XX-XX-XX XX:XX:XX,第一审判庭,1",
|
||||||
|
"n_fzje_level": 0,
|
||||||
|
"n_bqqpcje_level": 0,
|
||||||
|
"n_pcpcje_level": 0,
|
||||||
|
"c_ah": "(20XX)X省XX刑初XX号",
|
||||||
|
"c_gkws_pjjg": "一、被告人张某犯开设赌场罪,判处有期徒刑一年六个月,并处罚金人民币二万元。(刑期从判决执行之日起计算,判决执行以前先行羁押的,羁押一日折抵刑期一日,即自XXXX年X月XX日起至XXXX年X月XX日止。罚金在本判决生效后一个月内一次缴纳,期满不缴纳的,强制缴纳。)二、被告人李某犯开设赌场罪,判处有期徒刑一年二个月,并处罚金人民币二万元。(刑期从判决执行之日起计算,判决执行以前先行羁押的,羁押一日折抵刑期一日,即自XXXX年X月XX日起至XXXX年XX月XX日止。罚金在本判决生效后一个月内一次缴纳,期满不缴纳的,强制缴纳。)三、被告人王某犯开 设赌场罪,判处有期徒刑一年二个月,并处罚金人民币二万元。(刑期从判决执行之日起计算,判决执行以前先行羁押的,羁押一日折抵刑期一日,即自XXXX年XX月X日起至XXXX年X月X日止。 罚金在本判决生效后一个月内一次缴纳,期满不缴纳的,强制缴纳。)四、被告人赵某犯开设赌场罪,判处有期徒刑八个月,并处罚金人民币二万元。(刑期从判决执行之日起计算,判决执行 以前先行羁押的,羁押一日折抵刑期一日,即自XXXX年X月XX日起至XXXX年X月XX日止。罚金已缴纳。)五、被告人刘某犯开设赌场罪,判处有期徒刑六个月,并处罚金人民币一万五千元。(刑期从判决执行之日起计算,判决执行以前先行羁押的,羁押一日折抵刑期一日,即自XXXX年XX月XX日起至XXXX年X月XX日止。罚金已缴纳。)六、被告人陈某犯开设赌场罪,判处有期徒刑六个月,并处罚金人民币一万五千元。(刑期从判决执行之日起计算,判决执行以前先行羁押的,羁押一日折抵刑期一日,即自XXXX年XX月XX日起至XXXX年X月XX日止。罚金已缴纳。)如不服本判 决,可在收到判决书之次日起十日内,通过本院或直接向XX省XX市中级人民法院提出上诉。书面上诉的应提交上诉状正本一份,副本十三份。",
|
||||||
|
"c_ssdy": "XX省",
|
||||||
|
"n_ajbs": "xxxxxxxxxxxxxxxxxxxx",
|
||||||
|
"n_crc": 178414947,
|
||||||
|
"n_ajlx": "刑事一审",
|
||||||
|
"c_dsrxx": [
|
||||||
|
{
|
||||||
|
"n_ssdw": "被告人",
|
||||||
|
"c_mc": "张三",
|
||||||
|
"n_dsrlx": "自然人"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"n_ssdw": "被告人",
|
||||||
|
"c_mc": "李四",
|
||||||
|
"n_dsrlx": "自然人"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"n_ssdw": "被告人",
|
||||||
|
"c_mc": "王五",
|
||||||
|
"n_dsrlx": "自然人"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"n_ssdw": "被告人",
|
||||||
|
"c_mc": "赵六",
|
||||||
|
"n_dsrlx": "自然人"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"n_ssdw": "被告人",
|
||||||
|
"c_mc": "钱七",
|
||||||
|
"n_dsrlx": "自然人"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"n_ssdw": "被告人",
|
||||||
|
"c_mc": "孙八",
|
||||||
|
"n_dsrlx": "自然人"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"n_laay": "妨害社会管理秩序罪",
|
||||||
|
"d_larq": "20XX-XX-XX"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"n_laay_tree": "妨害社会管理秩序罪,扰乱公共秩序罪,开设赌场罪",
|
||||||
|
"n_jaay": "妨害社会管理秩序罪",
|
||||||
|
"n_ssdw": "上诉人",
|
||||||
|
"n_pcjg": "给予刑事处罚",
|
||||||
|
"n_jaay_tree": "妨害社会管理秩序罪,扰乱公共秩序罪,开设赌场罪",
|
||||||
|
"c_gkws_glah": "(20XX)X省XX刑初XX号",
|
||||||
|
"n_ccxzxje_level": 0,
|
||||||
|
"c_gkws_dsr": "原公诉机关XX省XX县人民检察院。上诉人(原审被告人)张三,农民。因涉嫌犯开设赌场罪于XXXX年X月XX日被刑事拘留,同年XX月XX日被逮捕。现羁押于XX县看守所。上诉人(原审被告人)李四,农民。因涉嫌犯开设赌场罪于XXXX年X月XX日被刑事拘留,同年XX月XX日被逮捕。现羁押于XX县看守所。上诉人(原审被告人) 王五,农民。因涉嫌犯开设赌场罪于XXXX年XX月X日被羁押,次日被刑事拘留,同月XX日被逮捕。现羁押于XX县看守所。原审被告人赵六,农民。因涉嫌犯开设赌场罪于XXXX年X月XX日被刑事拘留,同年XX月XX日被逮捕。现羁押于XX县看守所。原审被告人钱七,农民。因涉嫌犯开设赌场罪于XXXX年XX月XX日被刑事拘留,同年XX月XX日被逮捕。现羁押于XX县看守所。原审被告人 孙八,农民。因涉嫌犯开设赌场罪于XXXX年XX月XX日被刑事拘留,同年XX月XX日被逮捕。现羁押于XX县看守所。",
|
||||||
|
"n_jafs": "改判",
|
||||||
|
"n_jbfy_cj": "中级人民法院",
|
||||||
|
"c_id": "xxxxxxxxxxxxxxxxxxxx",
|
||||||
|
"n_slcx": "二审",
|
||||||
|
"n_ajjzjd": "已结案",
|
||||||
|
"n_jbfy": "XX省XX市中级人民法院",
|
||||||
|
"c_gkws_id": "xxxxxxxxxxxxxxxxxxxx",
|
||||||
|
"d_jarq": "20XX-XX-XX",
|
||||||
|
"n_ccxzxje_gj": 170000,
|
||||||
|
"n_ccxzxje_gj_level": 11,
|
||||||
|
"n_dzzm": "妨害社会管理秩序罪",
|
||||||
|
"n_fzje_level": 0,
|
||||||
|
"n_bqqpcje_level": 0,
|
||||||
|
"n_pcpcje_level": 0,
|
||||||
|
"c_ah": "(20XX)X省XX刑终XX号",
|
||||||
|
"c_gkws_pjjg": "一、维持XX省XX县人民法院(20XX)X省XX刑初XX号刑事判决的第四、第五、第六项,即:被告人赵六犯开设赌场罪,判处有期徒刑八个月,并处罚金人民币二万元。被告人钱七犯开设赌场罪,判处有期徒刑六个月,并处罚金人民币一万五千元。被告人孙八犯开设赌场罪,判处有期徒刑六个月,并处罚金人民币一万五千元。二、撤销XX省XX县人民法院(20XX)X省XX刑初XX号刑事判决的第一、第二、第三项,即:被告人张三犯开设赌场罪,判处有期徒刑一年六个月,并处罚金人民币二万元。被告人李四犯开设赌场罪,判处有期徒刑一年二个月,并处罚金人民币二万元。被告人王五犯开设赌场罪,判处有期徒刑一年二个月,并处罚金人民币二万元。三、上诉人(原审被告人)张三犯开设赌场罪,判处有期徒刑一年,并处罚金人民币二万元。(刑期从判决执行之日起计算。 判决执行前先行羁押的,羁押一日折抵刑期一日,即自XXXX年X月XX日起至XXXX年X月XX日止。罚金已缴纳。)四、上诉人(原审被告人)李四犯开设赌场罪,判处有期徒刑十个月,并处罚金人 民币二万元。(刑期从判决执行之日起计算。判决执行前先行羁押的,羁押一日折抵刑期一日,即自XXXX年X月XX日起至XXXX年X月XX日止;已缴纳罚金一万元,罚金余款自判决生效之次日起 一个月内缴纳,逾期不缴纳的,强制缴纳。)五、上诉人(原审被告人)王五犯开设赌场罪,判处有期徒刑十个月,并处罚金人民币二万元。(刑期从判决执行之日起计算。判决执行前先行羁押的,羁押一日折抵刑期一日,即自XXXX年XX月X日起至XXXX年X月X日止;已缴纳罚金一万元,罚金余款自判决生效之次日起一个月内缴纳,逾期不缴纳的,强制缴纳。)本判决为终审判决。",
|
||||||
|
"c_ssdy": "XX省",
|
||||||
|
"n_ajbs": "xxxxxxxxxxxxxxxxxxxx",
|
||||||
|
"n_dzzm_tree": "妨害社会管理秩序罪,扰乱公共秩序罪,开设赌场罪",
|
||||||
|
"n_crc": 3645650953,
|
||||||
|
"n_ajlx": "刑事二审",
|
||||||
|
"c_dsrxx": [
|
||||||
|
{
|
||||||
|
"n_ssdw": "其他",
|
||||||
|
"c_mc": "钱七",
|
||||||
|
"n_dsrlx": "自然人"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"n_ssdw": "其他",
|
||||||
|
"c_mc": "赵六",
|
||||||
|
"n_dsrlx": "自然人"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"n_ssdw": "其他",
|
||||||
|
"c_mc": "孙八",
|
||||||
|
"n_dsrlx": "自然人"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"n_ssdw": "上 诉人",
|
||||||
|
"c_mc": "张三",
|
||||||
|
"n_dsrlx": "自然人"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"n_ssdw": "上诉人",
|
||||||
|
"c_mc": "王五",
|
||||||
|
"n_dsrlx": "自然人"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"n_ssdw": "上诉人",
|
||||||
|
"c_mc": "李四",
|
||||||
|
"n_dsrlx": "自然人"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"n_laay": "妨害 社会管理秩序罪",
|
||||||
|
"d_larq": "20XX-XX-XX"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"n_laay_tree": "妨害社会管理秩序罪,扰乱公共秩序罪,开设赌场罪",
|
||||||
|
"n_jaay": "妨害社会管理秩序罪",
|
||||||
|
"n_ssdw": "被告人",
|
||||||
|
"n_pcjg": "给 予刑事处罚",
|
||||||
|
"n_jaay_tree": "妨害社会管理秩序罪,扰乱公共秩序罪,开设赌场罪",
|
||||||
|
"n_ccxzxje_level": 0,
|
||||||
|
"n_ssdw_ys": "被告人",
|
||||||
|
"c_gkws_dsr": "公诉机关XX省XX县人民检察院。被告人张三。被告人周明。",
|
||||||
|
"n_jafs": "判决",
|
||||||
|
"n_jbfy_cj": "基层法院",
|
||||||
|
"c_id": "xxxxxxxxxxxxxxxxxxxx",
|
||||||
|
"n_slcx": "一审",
|
||||||
|
"n_ajjzjd": "已结案",
|
||||||
|
"n_jbfy": "XX县人民法院",
|
||||||
|
"c_gkws_id": "xxxxxxxxxxxxxxxxxxxx",
|
||||||
|
"d_jarq": "20XX-XX-XX",
|
||||||
|
"c_slfsxx": "1,20XX-XX-XX XX:XX:XX,KA6第一审判庭,1",
|
||||||
|
"n_dzzm": "妨害社会管理秩序罪",
|
||||||
|
"n_fzje_level": 0,
|
||||||
|
"n_bqqpcje_level": 0,
|
||||||
|
"n_pcpcje_level": 0,
|
||||||
|
"c_ah": "(20XX)X省XX刑初XX号",
|
||||||
|
"c_gkws_pjjg": "一、被告人张三犯开设赌场罪,判处有期徒刑二年,并处罚金人民币二万元。(刑期从判决执行之日起计算,判决 执行以前先行羁押的,羁押一日折抵刑期一日,即自XXXX年XX月XX日起至XXXX年XX月XX日止。罚金在本判决生效后一个月内一次缴纳,期满不缴纳的,强制缴纳。)二、被告人周明犯开设赌 场罪,判处有期徒刑一年六个月,并处罚金人民币一万五千元。(刑期从判决执行之日起计算,判决执行以前先行羁押的,羁押一日折抵刑期一日,即自XXXX年XX月XX日起至XXXX年X月XX日止 。罚金在本判决生效后一个月内一次缴纳,期满不缴纳的,强制缴纳。)三、扣押在案的赌具扑克牌、龙虎珠、骨牌及人民币八百三十五元依法予以没收,其中人民币八百三十五元上缴国库。如不服本判决,可在收到判决书之次日起十日内,通过本院或直接向XX省XX市中级人民法院提出上诉。书面上诉的应当提交上诉状正本一份,副本九份。",
|
||||||
|
"c_ssdy": "XX省",
|
||||||
|
"n_ajbs": "xxxxxxxxxxxxxxxxxxxx",
|
||||||
|
"n_dzzm_tree": "妨害社会管理秩序罪,扰乱公共秩序罪,开设赌场罪",
|
||||||
|
"n_crc": 3676144743,
|
||||||
|
"n_ajlx": "刑事一审",
|
||||||
|
"c_dsrxx": [
|
||||||
|
{
|
||||||
|
"n_ssdw": "被告人",
|
||||||
|
"c_mc": "张 三",
|
||||||
|
"n_dsrlx": "自然人"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"n_ssdw": "被告人",
|
||||||
|
"c_mc": "周明",
|
||||||
|
"n_dsrlx": "自然人"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"n_laay": "妨害社会管理秩序罪",
|
||||||
|
"d_larq": "20XX-XX-XX"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"n_laay_tree": "妨害社会管理秩 序罪,扰乱公共秩序罪,开设赌场罪",
|
||||||
|
"n_jaay": "妨害社会管理秩序罪",
|
||||||
|
"n_ssdw": "上诉人",
|
||||||
|
"n_pcjg": "给予刑事处罚",
|
||||||
|
"n_jaay_tree": "妨害社会管理秩序罪,扰乱公共秩序罪,开设赌场 罪",
|
||||||
|
"c_gkws_glah": "(20XX)X省XX刑初XX号",
|
||||||
|
"n_ccxzxje_level": 0,
|
||||||
|
"c_gkws_dsr": "原公诉机关XX省XX县人民检察院。上诉人(原审被告人)张三,男,XXXX年X月XX日出生于XX省XX县,XX族,小学文化,农民,住XX省XX县。因本案于XXXX年XX月XX日被抓获,次日被行政拘留,因涉嫌犯开设赌场罪,于同月XX日被刑事拘留,同年XX月X日被逮捕。现羁押于XX县看守所。上诉人(原 审被告人)周明,男,XXXX年X月XX日出生于XX省XX县,XX族,小学文化,农民,住XX省XX县。因涉嫌犯开设赌场罪,于XXXX年XX月XX日被刑事拘留,同年XX月XX日被逮捕。现羁押于XX县看守所。",
|
||||||
|
"n_jafs": "维持",
|
||||||
|
"n_jbfy_cj": "中级人民法院",
|
||||||
|
"c_id": "xxxxxxxxxxxxxxxxxxxx",
|
||||||
|
"n_slcx": "二审",
|
||||||
|
"n_ajjzjd": "已结案",
|
||||||
|
"n_jbfy": "XX省XX市中级人民法院",
|
||||||
|
"c_gkws_id": "xxxxxxxxxxxxxxxxxxxx",
|
||||||
|
"d_jarq": "20XX-XX-XX",
|
||||||
|
"c_slfsxx": "1,,,1",
|
||||||
|
"n_dzzm": "妨害社会管理秩序罪",
|
||||||
|
"n_fzje_level": 0,
|
||||||
|
"n_bqqpcje_level": 0,
|
||||||
|
"n_pcpcje_level": 0,
|
||||||
|
"c_ah": "(20XX)X省XX刑终XX号",
|
||||||
|
"c_gkws_pjjg": "驳回上诉,维持原判。本裁定为终审裁定。",
|
||||||
|
"c_ssdy": "XX省",
|
||||||
|
"n_ajbs": "xxxxxxxxxxxxxxxxxxxx",
|
||||||
|
"n_dzzm_tree": "妨害社会管理秩序罪,扰乱公共秩序罪,开设赌场罪",
|
||||||
|
"n_crc": 3173700612,
|
||||||
|
"n_ajlx": "刑事二审",
|
||||||
|
"c_dsrxx": [
|
||||||
|
{
|
||||||
|
"n_ssdw": "上诉人",
|
||||||
|
"c_mc": "张三",
|
||||||
|
"n_dsrlx": "自然人"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"n_ssdw": "上诉人",
|
||||||
|
"c_mc": "周明",
|
||||||
|
"n_dsrlx": "自然人"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"n_laay": "妨害社会管理秩序罪",
|
||||||
|
"d_larq": "20XX-XX-XX"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"count": {
|
||||||
|
"money_jie_total": 11,
|
||||||
|
"count_total": 4,
|
||||||
|
"larq_stat": "20XX(2),20XX(2)",
|
||||||
|
"money_wei_percent": 0,
|
||||||
|
"area_stat": "XX省(4)",
|
||||||
|
"money_jie_beigao": 0,
|
||||||
|
"count_jie_total": 4,
|
||||||
|
"money_jie_other": 0,
|
||||||
|
"count_wei_total": 0,
|
||||||
|
"count_jie_beigao": 2,
|
||||||
|
"money_yuangao": 11,
|
||||||
|
"money_beigao": 0,
|
||||||
|
"ay_stat": "妨害社会管理秩序罪(4)",
|
||||||
|
"count_wei_other": 0,
|
||||||
|
"count_wei_beigao": 0,
|
||||||
|
"count_wei_yuangao": 0,
|
||||||
|
"money_other": 0,
|
||||||
|
"count_yuangao": 2,
|
||||||
|
"money_wei_yuangao": 0,
|
||||||
|
"money_jie_yuangao": 11,
|
||||||
|
"money_wei_beigao": 0,
|
||||||
|
"count_jie_yuangao": 2,
|
||||||
|
"count_other": 0,
|
||||||
|
"count_jie_other": 0,
|
||||||
|
"count_beigao": 2,
|
||||||
|
"money_wei_total": 0,
|
||||||
|
"money_wei_other": 0,
|
||||||
|
"money_total": 11,
|
||||||
|
"jafs_stat": "判决(2),维持(1),改判(1)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"bankrupt": {}
|
||||||
|
}'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
xgbzxr: [
|
||||||
|
{ msg: '没有找到', xgbzxr: '[]'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
217
pkg/jwt/jwtx_test.go
Normal file
217
pkg/jwt/jwtx_test.go
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
package jwtx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGenerateJwtToken(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
userId int64
|
||||||
|
secret string
|
||||||
|
expireTime int64
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "正常情况 - 生成有效token",
|
||||||
|
userId: 144,
|
||||||
|
secret: "Mf5Xph3PoyKzVpRw0Zy1+X4uR/tM7JvGMEV/5p2M/tU=",
|
||||||
|
expireTime: 3600, // 1小时
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "用户ID为0",
|
||||||
|
userId: 0,
|
||||||
|
secret: "test-secret-key",
|
||||||
|
expireTime: 3600,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "用户ID为负数",
|
||||||
|
userId: -1,
|
||||||
|
secret: "test-secret-key",
|
||||||
|
expireTime: 3600,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "过期时间为0",
|
||||||
|
userId: 12345,
|
||||||
|
secret: "test-secret-key",
|
||||||
|
expireTime: 0,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "过期时间为负数",
|
||||||
|
userId: 12345,
|
||||||
|
secret: "test-secret-key",
|
||||||
|
expireTime: -3600,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "空密钥",
|
||||||
|
userId: 12345,
|
||||||
|
secret: "",
|
||||||
|
expireTime: 3600,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "长密钥",
|
||||||
|
userId: 12345,
|
||||||
|
secret: "very-long-secret-key-that-is-more-than-32-characters-long",
|
||||||
|
expireTime: 3600,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
token, err := GenerateJwtToken(tt.userId, tt.secret, tt.expireTime)
|
||||||
|
fmt.Printf("tt.userId: %d, token: %s\n", tt.userId, token)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("GenerateJwtToken() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !tt.wantErr {
|
||||||
|
// 验证token不为空
|
||||||
|
if token == "" {
|
||||||
|
t.Error("GenerateJwtToken() returned empty token")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证token长度合理
|
||||||
|
if len(token) < 10 {
|
||||||
|
t.Errorf("GenerateJwtToken() returned token too short: %s", token)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证可以解析token(对于过期时间为0或负数的情况,token会立即过期)
|
||||||
|
if tt.expireTime <= 0 {
|
||||||
|
// 对于立即过期的token,我们只验证生成成功,不验证解析
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedUserId, parseErr := ParseJwtToken(token, tt.secret)
|
||||||
|
if parseErr != nil {
|
||||||
|
t.Errorf("Failed to parse generated token: %v", parseErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if parsedUserId != tt.userId {
|
||||||
|
t.Errorf("Parsed userId = %v, want %v", parsedUserId, tt.userId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGenerateJwtToken_ExpirationTime(t *testing.T) {
|
||||||
|
userId := int64(12345)
|
||||||
|
secret := "test-secret-key"
|
||||||
|
expireTime := int64(3600) // 1小时
|
||||||
|
|
||||||
|
token, err := GenerateJwtToken(userId, secret, expireTime)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GenerateJwtToken() failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证token可以立即解析
|
||||||
|
parsedUserId, err := ParseJwtToken(token, secret)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to parse token immediately: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if parsedUserId != userId {
|
||||||
|
t.Errorf("Parsed userId = %v, want %v", parsedUserId, userId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGenerateJwtToken_Consistency(t *testing.T) {
|
||||||
|
userId := int64(12345)
|
||||||
|
secret := "test-secret-key"
|
||||||
|
expireTime := int64(3600)
|
||||||
|
|
||||||
|
// 生成多个token,验证相同参数生成的token是一致的
|
||||||
|
var firstToken string
|
||||||
|
|
||||||
|
for i := 0; i < 5; i++ {
|
||||||
|
token, err := GenerateJwtToken(userId, secret, expireTime)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GenerateJwtToken() failed on iteration %d: %v", i, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if i == 0 {
|
||||||
|
firstToken = token
|
||||||
|
} else {
|
||||||
|
// 由于JWT基于相同claims生成,相同参数应该产生相同的token
|
||||||
|
// 但由于时间戳可能略有差异,我们验证token长度和格式一致
|
||||||
|
if len(token) != len(firstToken) {
|
||||||
|
t.Errorf("Token length inconsistent: got %d, want %d", len(token), len(firstToken))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGenerateJwtToken_LargeUserId(t *testing.T) {
|
||||||
|
// 测试大数值的userId
|
||||||
|
largeUserId := int64(999999999999999) // 使用一个大的但合理的值
|
||||||
|
secret := "test-secret-key"
|
||||||
|
expireTime := int64(3600)
|
||||||
|
|
||||||
|
token, err := GenerateJwtToken(largeUserId, secret, expireTime)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GenerateJwtToken() failed with large userId: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedUserId, err := ParseJwtToken(token, secret)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to parse token with large userId: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if parsedUserId != largeUserId {
|
||||||
|
t.Errorf("Parsed userId = %v, want %v", parsedUserId, largeUserId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGenerateJwtToken_Concurrent(t *testing.T) {
|
||||||
|
userId := int64(12345)
|
||||||
|
secret := "test-secret-key"
|
||||||
|
expireTime := int64(3600)
|
||||||
|
|
||||||
|
// 并发测试
|
||||||
|
done := make(chan bool, 10)
|
||||||
|
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
go func() {
|
||||||
|
token, err := GenerateJwtToken(userId, secret, expireTime)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("GenerateJwtToken() failed in goroutine: %v", err)
|
||||||
|
done <- false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedUserId, err := ParseJwtToken(token, secret)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Failed to parse token in goroutine: %v", err)
|
||||||
|
done <- false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if parsedUserId != userId {
|
||||||
|
t.Errorf("Parsed userId = %v, want %v in goroutine", parsedUserId, userId)
|
||||||
|
done <- false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
done <- true
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 等待所有goroutine完成
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
if !<-done {
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
544
validator封装设计.md
Normal file
544
validator封装设计.md
Normal file
@@ -0,0 +1,544 @@
|
|||||||
|
# Validator 封装设计
|
||||||
|
|
||||||
|
## 1. 整体架构设计
|
||||||
|
|
||||||
|
```
|
||||||
|
shared/
|
||||||
|
├── validator/ # 格式校验器封装
|
||||||
|
│ ├── validator.go # 校验器初始化和接口定义
|
||||||
|
│ ├── format_validator.go # 格式校验实现
|
||||||
|
│ ├── custom_rules.go # 自定义校验规则
|
||||||
|
│ ├── messages.go # 错误消息配置
|
||||||
|
│ └── middleware.go # 校验中间件
|
||||||
|
├── errcode/
|
||||||
|
│ ├── validator_errors.go # 校验相关错误码
|
||||||
|
└── response/
|
||||||
|
└── validator_response.go # 校验错误响应格式
|
||||||
|
|
||||||
|
domains/
|
||||||
|
└── product/rpc/internal/logic/
|
||||||
|
└── validator/ # 业务校验器封装
|
||||||
|
├── base.go # 业务校验器基类
|
||||||
|
├── product_validator.go
|
||||||
|
└── category_validator.go
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. 格式校验器封装(shared/validator)
|
||||||
|
|
||||||
|
### 2.1 校验器接口定义
|
||||||
|
|
||||||
|
```go
|
||||||
|
// shared/validator/validator.go
|
||||||
|
package validator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
|
"tianyuan/shared/errcode"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 校验器接口
|
||||||
|
type IValidator interface {
|
||||||
|
Validate(data interface{}) error
|
||||||
|
ValidateStruct(data interface{}) error
|
||||||
|
AddCustomRule(tag string, fn validator.Func) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// 全局校验器实例
|
||||||
|
var GlobalValidator IValidator
|
||||||
|
|
||||||
|
// 初始化校验器
|
||||||
|
func Init() error {
|
||||||
|
v := &FormatValidator{
|
||||||
|
validator: validator.New(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// 注册自定义规则
|
||||||
|
if err := v.registerCustomRules(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 注册中文错误消息
|
||||||
|
if err := v.registerMessages(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
GlobalValidator = v
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 便捷函数
|
||||||
|
func Validate(data interface{}) error {
|
||||||
|
return GlobalValidator.Validate(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ValidateStruct(data interface{}) error {
|
||||||
|
return GlobalValidator.ValidateStruct(data)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 格式校验器实现
|
||||||
|
|
||||||
|
```go
|
||||||
|
// shared/validator/format_validator.go
|
||||||
|
package validator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
|
"tianyuan/shared/errcode"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FormatValidator struct {
|
||||||
|
validator *validator.Validate
|
||||||
|
}
|
||||||
|
|
||||||
|
// 校验接口实现
|
||||||
|
func (v *FormatValidator) Validate(data interface{}) error {
|
||||||
|
if err := v.validator.Struct(data); err != nil {
|
||||||
|
return v.formatError(err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *FormatValidator) ValidateStruct(data interface{}) error {
|
||||||
|
return v.Validate(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *FormatValidator) AddCustomRule(tag string, fn validator.Func) error {
|
||||||
|
return v.validator.RegisterValidation(tag, fn)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 错误格式化
|
||||||
|
func (v *FormatValidator) formatError(err error) error {
|
||||||
|
if validationErrors, ok := err.(validator.ValidationErrors); ok {
|
||||||
|
var errMsgs []string
|
||||||
|
|
||||||
|
for _, fieldError := range validationErrors {
|
||||||
|
errMsg := v.getErrorMessage(fieldError)
|
||||||
|
errMsgs = append(errMsgs, errMsg)
|
||||||
|
}
|
||||||
|
|
||||||
|
return errcode.NewValidationError(strings.Join(errMsgs, "; "))
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取错误消息
|
||||||
|
func (v *FormatValidator) getErrorMessage(fieldError validator.FieldError) string {
|
||||||
|
// 获取字段的中文名称
|
||||||
|
fieldName := v.getFieldName(fieldError)
|
||||||
|
|
||||||
|
// 根据校验标签获取错误消息
|
||||||
|
switch fieldError.Tag() {
|
||||||
|
case "required":
|
||||||
|
return fmt.Sprintf("%s不能为空", fieldName)
|
||||||
|
case "min":
|
||||||
|
return fmt.Sprintf("%s最小值为%s", fieldName, fieldError.Param())
|
||||||
|
case "max":
|
||||||
|
return fmt.Sprintf("%s最大值为%s", fieldName, fieldError.Param())
|
||||||
|
case "email":
|
||||||
|
return fmt.Sprintf("%s格式不正确", fieldName)
|
||||||
|
case "mobile":
|
||||||
|
return fmt.Sprintf("%s格式不正确", fieldName)
|
||||||
|
default:
|
||||||
|
return fmt.Sprintf("%s校验失败", fieldName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取字段中文名称
|
||||||
|
func (v *FormatValidator) getFieldName(fieldError validator.FieldError) string {
|
||||||
|
// 可以通过反射获取struct tag中的中文名称
|
||||||
|
// 或者维护一个字段名映射表
|
||||||
|
fieldName := fieldError.Field()
|
||||||
|
|
||||||
|
// 简单示例,实际可以更复杂
|
||||||
|
nameMap := map[string]string{
|
||||||
|
"CategoryId": "分类ID",
|
||||||
|
"PageNum": "页码",
|
||||||
|
"PageSize": "每页数量",
|
||||||
|
"Keyword": "关键词",
|
||||||
|
"Mobile": "手机号",
|
||||||
|
"Email": "邮箱",
|
||||||
|
}
|
||||||
|
|
||||||
|
if chineseName, exists := nameMap[fieldName]; exists {
|
||||||
|
return chineseName
|
||||||
|
}
|
||||||
|
|
||||||
|
return fieldName
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.3 自定义校验规则
|
||||||
|
|
||||||
|
```go
|
||||||
|
// shared/validator/custom_rules.go
|
||||||
|
package validator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"regexp"
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 注册自定义校验规则
|
||||||
|
func (v *FormatValidator) registerCustomRules() error {
|
||||||
|
// 手机号校验
|
||||||
|
if err := v.validator.RegisterValidation("mobile", validateMobile); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 身份证号校验
|
||||||
|
if err := v.validator.RegisterValidation("idcard", validateIDCard); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 企业统一社会信用代码校验
|
||||||
|
if err := v.validator.RegisterValidation("creditcode", validateCreditCode); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 手机号校验函数
|
||||||
|
func validateMobile(fl validator.FieldLevel) bool {
|
||||||
|
mobile := fl.Field().String()
|
||||||
|
pattern := `^1[3-9]\d{9}$`
|
||||||
|
matched, _ := regexp.MatchString(pattern, mobile)
|
||||||
|
return matched
|
||||||
|
}
|
||||||
|
|
||||||
|
// 身份证号校验函数
|
||||||
|
func validateIDCard(fl validator.FieldLevel) bool {
|
||||||
|
idcard := fl.Field().String()
|
||||||
|
pattern := `^[1-9]\d{5}(18|19|20)\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$`
|
||||||
|
matched, _ := regexp.MatchString(pattern, idcard)
|
||||||
|
return matched
|
||||||
|
}
|
||||||
|
|
||||||
|
// 企业统一社会信用代码校验函数
|
||||||
|
func validateCreditCode(fl validator.FieldLevel) bool {
|
||||||
|
code := fl.Field().String()
|
||||||
|
pattern := `^[0-9A-HJ-NPQRTUWXY]{2}\d{6}[0-9A-HJ-NPQRTUWXY]{10}$`
|
||||||
|
matched, _ := regexp.MatchString(pattern, code)
|
||||||
|
return matched
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.4 校验中间件
|
||||||
|
|
||||||
|
```go
|
||||||
|
// shared/validator/middleware.go
|
||||||
|
package validator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
|
"tianyuan/shared/response"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 校验中间件
|
||||||
|
func ValidationMiddleware() func(http.Handler) http.Handler {
|
||||||
|
return func(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// 这里可以添加全局校验逻辑
|
||||||
|
// 比如请求头校验、通用参数校验等
|
||||||
|
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handler校验辅助函数
|
||||||
|
func ValidateAndParse(r *http.Request, req interface{}) error {
|
||||||
|
// 1. 参数绑定
|
||||||
|
if err := httpx.Parse(r, req); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 格式校验
|
||||||
|
if err := Validate(req); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. 业务校验器封装(各域内部)
|
||||||
|
|
||||||
|
### 3.1 业务校验器基类
|
||||||
|
|
||||||
|
```go
|
||||||
|
// domains/product/rpc/internal/logic/validator/base.go
|
||||||
|
package validator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"tianyuan/domains/product/rpc/internal/svc"
|
||||||
|
"tianyuan/shared/errcode"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 业务校验器基类
|
||||||
|
type BaseValidator struct {
|
||||||
|
svcCtx *svc.ServiceContext
|
||||||
|
ctx context.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewBaseValidator(ctx context.Context, svcCtx *svc.ServiceContext) *BaseValidator {
|
||||||
|
return &BaseValidator{
|
||||||
|
svcCtx: svcCtx,
|
||||||
|
ctx: ctx,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 业务校验接口
|
||||||
|
type IBusinessValidator interface {
|
||||||
|
ValidatePermission(userId int64, action string) error
|
||||||
|
ValidateResourceExists(resourceType string, resourceId int64) error
|
||||||
|
ValidateBusinessRules(data interface{}) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通用业务校验方法
|
||||||
|
func (v *BaseValidator) ValidatePermission(userId int64, action string) error {
|
||||||
|
// 调用用户域RPC检查权限
|
||||||
|
resp, err := v.svcCtx.UserRpc.CheckPermission(v.ctx, &user.CheckPermissionReq{
|
||||||
|
UserId: userId,
|
||||||
|
Permission: action,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !resp.HasPermission {
|
||||||
|
return errcode.NewBusinessError("用户无权限执行此操作")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *BaseValidator) ValidateResourceExists(resourceType string, resourceId int64) error {
|
||||||
|
// 根据资源类型检查资源是否存在
|
||||||
|
switch resourceType {
|
||||||
|
case "category":
|
||||||
|
return v.validateCategoryExists(resourceId)
|
||||||
|
case "product":
|
||||||
|
return v.validateProductExists(resourceId)
|
||||||
|
default:
|
||||||
|
return errcode.NewBusinessError("未知的资源类型")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *BaseValidator) validateCategoryExists(categoryId int64) error {
|
||||||
|
category, err := v.svcCtx.CategoryModel.FindOne(v.ctx, categoryId)
|
||||||
|
if err != nil {
|
||||||
|
return errcode.NewBusinessError("分类不存在")
|
||||||
|
}
|
||||||
|
|
||||||
|
if category.Status != 1 {
|
||||||
|
return errcode.NewBusinessError("分类已禁用")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *BaseValidator) validateProductExists(productId int64) error {
|
||||||
|
product, err := v.svcCtx.ProductModel.FindOne(v.ctx, productId)
|
||||||
|
if err != nil {
|
||||||
|
return errcode.NewBusinessError("产品不存在")
|
||||||
|
}
|
||||||
|
|
||||||
|
if product.Status != 1 {
|
||||||
|
return errcode.NewBusinessError("产品已下架")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 具体业务校验器
|
||||||
|
|
||||||
|
```go
|
||||||
|
// domains/product/rpc/internal/logic/validator/product_validator.go
|
||||||
|
package validator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"tianyuan/domains/product/rpc/internal/svc"
|
||||||
|
"tianyuan/domains/product/rpc/product"
|
||||||
|
"tianyuan/shared/errcode"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProductValidator struct {
|
||||||
|
*BaseValidator
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewProductValidator(ctx context.Context, svcCtx *svc.ServiceContext) *ProductValidator {
|
||||||
|
return &ProductValidator{
|
||||||
|
BaseValidator: NewBaseValidator(ctx, svcCtx),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 校验获取产品列表请求
|
||||||
|
func (v *ProductValidator) ValidateGetProductListRequest(req *product.GetProductListReq) error {
|
||||||
|
// 1. 校验用户权限
|
||||||
|
if err := v.ValidatePermission(req.UserId, "product:list"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 校验分类存在性
|
||||||
|
if req.CategoryId > 0 {
|
||||||
|
if err := v.ValidateResourceExists("category", req.CategoryId); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 校验用户是否有查看该分类的权限
|
||||||
|
if req.CategoryId > 0 {
|
||||||
|
if err := v.validateCategoryAccess(req.UserId, req.CategoryId); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 校验创建产品请求
|
||||||
|
func (v *ProductValidator) ValidateCreateProductRequest(req *product.CreateProductReq) error {
|
||||||
|
// 1. 校验用户权限
|
||||||
|
if err := v.ValidatePermission(req.UserId, "product:create"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 校验分类存在性
|
||||||
|
if err := v.ValidateResourceExists("category", req.CategoryId); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 校验产品名称是否重复
|
||||||
|
if err := v.validateProductNameUnique(req.Name, req.CategoryId); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 校验分类访问权限
|
||||||
|
func (v *ProductValidator) validateCategoryAccess(userId, categoryId int64) error {
|
||||||
|
// 检查用户是否有访问该分类的权限
|
||||||
|
// 这里可能涉及到用户等级、VIP权限等业务逻辑
|
||||||
|
userInfo, err := v.svcCtx.UserRpc.GetUserInfo(v.ctx, &user.GetUserInfoReq{
|
||||||
|
UserId: userId,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 示例:VIP用户可以访问所有分类,普通用户只能访问基础分类
|
||||||
|
category, err := v.svcCtx.CategoryModel.FindOne(v.ctx, categoryId)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if category.RequireVip && userInfo.UserType != "vip" {
|
||||||
|
return errcode.NewBusinessError("该分类需要VIP权限")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 校验产品名称唯一性
|
||||||
|
func (v *ProductValidator) validateProductNameUnique(name string, categoryId int64) error {
|
||||||
|
exists, err := v.svcCtx.ProductModel.CheckNameExists(v.ctx, name, categoryId)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if exists {
|
||||||
|
return errcode.NewBusinessError("同分类下产品名称已存在")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. 使用示例
|
||||||
|
|
||||||
|
### 4.1 在 Handler 中使用格式校验
|
||||||
|
|
||||||
|
```go
|
||||||
|
// client/internal/handler/product/getproductlisthandler.go
|
||||||
|
func (h *GetProductListHandler) GetProductList(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req types.GetProductListReq
|
||||||
|
|
||||||
|
// 使用封装的校验函数
|
||||||
|
if err := validator.ValidateAndParse(r, &req); err != nil {
|
||||||
|
response.ParamErrorResponse(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用Logic层
|
||||||
|
resp, err := h.logic.GetProductList(&req)
|
||||||
|
if err != nil {
|
||||||
|
response.ErrorResponse(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.SuccessResponse(w, resp)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 在 RPC Logic 中使用业务校验
|
||||||
|
|
||||||
|
```go
|
||||||
|
// domains/product/rpc/internal/logic/getproductlistlogic.go
|
||||||
|
func (l *GetProductListLogic) GetProductList(req *product.GetProductListReq) (*product.GetProductListResp, error) {
|
||||||
|
// 创建业务校验器
|
||||||
|
validator := validator.NewProductValidator(l.ctx, l.svcCtx)
|
||||||
|
|
||||||
|
// 执行业务校验
|
||||||
|
if err := validator.ValidateGetProductListRequest(req); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行业务逻辑
|
||||||
|
products, total, err := l.svcCtx.ProductModel.FindList(l.ctx, req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &product.GetProductListResp{
|
||||||
|
List: products,
|
||||||
|
Total: total,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3 在 main.go 中初始化
|
||||||
|
|
||||||
|
```go
|
||||||
|
// client/client.go
|
||||||
|
func main() {
|
||||||
|
// 初始化格式校验器
|
||||||
|
if err := validator.Init(); err != nil {
|
||||||
|
log.Fatalf("初始化校验器失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 其他初始化代码...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. 封装的优势
|
||||||
|
|
||||||
|
1. **统一的接口**:所有校验都通过统一的接口调用
|
||||||
|
2. **错误处理标准化**:统一的错误格式和消息
|
||||||
|
3. **可扩展性**:容易添加新的校验规则
|
||||||
|
4. **代码复用**:通用校验逻辑可以复用
|
||||||
|
5. **测试友好**:校验器可以独立测试
|
||||||
|
6. **配置化**:错误消息和校验规则可以配置化管理
|
||||||
|
|
||||||
|
这样的封装既保持了格式校验和业务校验的清晰边界,又提供了便捷的使用接口。
|
||||||
405
域与服务的关系及目录架构.md
Normal file
405
域与服务的关系及目录架构.md
Normal file
@@ -0,0 +1,405 @@
|
|||||||
|
# 域与服务的关系及目录架构设计
|
||||||
|
|
||||||
|
## 🤔 域 vs 服务的关系
|
||||||
|
|
||||||
|
### 核心概念
|
||||||
|
|
||||||
|
- **域 (Domain)** = **业务边界**,是逻辑概念
|
||||||
|
- **服务 (Service)** = **技术实现**,是物理部署单元
|
||||||
|
|
||||||
|
### 关系图解
|
||||||
|
|
||||||
|
```
|
||||||
|
Domain (域)
|
||||||
|
├── Service A (微服务A)
|
||||||
|
├── Service B (微服务B)
|
||||||
|
└── Service C (微服务C)
|
||||||
|
```
|
||||||
|
|
||||||
|
**一个域可以包含多个微服务,也可以是一个微服务。**
|
||||||
|
|
||||||
|
## 🏗️ 具体的域-服务映射
|
||||||
|
|
||||||
|
### 方案一:一域一服务 (推荐给中小型项目)
|
||||||
|
|
||||||
|
```
|
||||||
|
├── user-domain/ # 用户域
|
||||||
|
│ └── user-service/ # 用户服务 (包含用户+企业+认证)
|
||||||
|
├── security-domain/ # 安全域
|
||||||
|
│ └── security-service/ # 安全服务 (加密+白名单+密钥)
|
||||||
|
├── product-domain/ # 产品域
|
||||||
|
│ └── product-service/ # 产品服务 (产品管理+权限控制)
|
||||||
|
├── data-domain/ # 数据服务域
|
||||||
|
│ └── data-service/ # 数据服务 (统一数据网关)
|
||||||
|
├── billing-domain/ # 计费域
|
||||||
|
│ └── billing-service/ # 计费服务 (钱包+扣费+账单)
|
||||||
|
├── audit-domain/ # 审计域
|
||||||
|
│ └── audit-service/ # 审计服务 (日志+记录)
|
||||||
|
└── gateway-domain/ # 网关域
|
||||||
|
└── gateway-service/ # 网关服务
|
||||||
|
```
|
||||||
|
|
||||||
|
### 方案二:一域多服务 (适合大型项目)
|
||||||
|
|
||||||
|
```
|
||||||
|
├── user-domain/ # 用户域
|
||||||
|
│ ├── user-service/ # 用户管理服务
|
||||||
|
│ ├── enterprise-service/ # 企业认证服务
|
||||||
|
│ └── auth-service/ # 认证授权服务
|
||||||
|
├── security-domain/ # 安全域
|
||||||
|
│ ├── encryption-service/ # 加密解密服务
|
||||||
|
│ ├── whitelist-service/ # 白名单服务
|
||||||
|
│ └── key-management-service/ # 密钥管理服务
|
||||||
|
├── data-domain/ # 数据服务域
|
||||||
|
│ ├── data-gateway-service/ # 数据网关服务
|
||||||
|
│ ├── risk-service/ # 风险评估服务 (原FLXG)
|
||||||
|
│ ├── credit-service/ # 征信服务 (原JRZQ)
|
||||||
|
│ ├── company-service/ # 企业信息服务 (原QYGL)
|
||||||
|
│ └── query-service/ # 数据查询服务 (原IVYZ)
|
||||||
|
└── ...
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📁 推荐的目录架构设计
|
||||||
|
|
||||||
|
### 整体项目结构 (基于你的项目)
|
||||||
|
|
||||||
|
```
|
||||||
|
tianyuan-api-platform/
|
||||||
|
├── domains/ # 业务域目录
|
||||||
|
│ ├── user-domain/ # 用户域
|
||||||
|
│ │ └── user-service/ # 用户服务
|
||||||
|
│ │ ├── cmd/ # 应用入口
|
||||||
|
│ │ │ ├── server/main.go # 服务器入口
|
||||||
|
│ │ │ ├── cli/main.go # CLI工具
|
||||||
|
│ │ │ └── migrator/main.go # 数据库迁移
|
||||||
|
│ │ ├── api/ # API定义
|
||||||
|
│ │ │ ├── grpc/ # gRPC定义
|
||||||
|
│ │ │ │ └── user.proto
|
||||||
|
│ │ │ └── http/ # HTTP API定义
|
||||||
|
│ │ │ └── user.api
|
||||||
|
│ │ ├── internal/ # 内部实现
|
||||||
|
│ │ │ ├── domain/ # 领域层
|
||||||
|
│ │ │ │ ├── entity/ # 实体
|
||||||
|
│ │ │ │ │ ├── user.go
|
||||||
|
│ │ │ │ │ └── enterprise.go
|
||||||
|
│ │ │ │ ├── valueobject/ # 值对象
|
||||||
|
│ │ │ │ ├── repository/ # 仓储接口
|
||||||
|
│ │ │ │ │ └── user_repository.go
|
||||||
|
│ │ │ │ └── service/ # 领域服务
|
||||||
|
│ │ │ │ └── user_domain_service.go
|
||||||
|
│ │ │ ├── application/ # 应用层
|
||||||
|
│ │ │ │ ├── service/ # 应用服务
|
||||||
|
│ │ │ │ │ └── user_app_service.go
|
||||||
|
│ │ │ │ ├── dto/ # 数据传输对象
|
||||||
|
│ │ │ │ └── command/ # 命令处理
|
||||||
|
│ │ │ ├── infrastructure/ # 基础设施层
|
||||||
|
│ │ │ │ ├── persistence/ # 持久化
|
||||||
|
│ │ │ │ │ ├── mysql/
|
||||||
|
│ │ │ │ │ │ └── user_repository_impl.go
|
||||||
|
│ │ │ │ │ └── redis/
|
||||||
|
│ │ │ │ ├── messaging/ # 消息队列
|
||||||
|
│ │ │ │ └── external/ # 外部服务
|
||||||
|
│ │ │ └── interfaces/ # 接口适配器
|
||||||
|
│ │ │ ├── grpc/ # gRPC适配器
|
||||||
|
│ │ │ │ └── user_grpc_handler.go
|
||||||
|
│ │ │ ├── http/ # HTTP适配器
|
||||||
|
│ │ │ │ └── user_http_handler.go
|
||||||
|
│ │ │ └── event/ # 事件处理
|
||||||
|
│ │ ├── configs/ # 配置文件
|
||||||
|
│ │ │ ├── config.yaml
|
||||||
|
│ │ │ └── config.dev.yaml
|
||||||
|
│ │ ├── deployments/ # 部署配置
|
||||||
|
│ │ │ ├── Dockerfile
|
||||||
|
│ │ │ └── k8s/
|
||||||
|
│ │ ├── docs/ # 文档
|
||||||
|
│ │ ├── test/ # 测试
|
||||||
|
│ │ └── go.mod
|
||||||
|
│ │
|
||||||
|
│ ├── data-domain/ # 数据服务域
|
||||||
|
│ │ └── data-service/ # 数据服务 (主要协调者)
|
||||||
|
│ │ ├── cmd/
|
||||||
|
│ │ │ └── server/main.go
|
||||||
|
│ │ ├── api/
|
||||||
|
│ │ │ └── http/
|
||||||
|
│ │ │ └── data.api
|
||||||
|
│ │ ├── internal/
|
||||||
|
│ │ │ ├── domain/
|
||||||
|
│ │ │ │ └── service/
|
||||||
|
│ │ │ │ ├── risk_assessment_service.go # 原FLXG逻辑
|
||||||
|
│ │ │ │ ├── credit_check_service.go # 原JRZQ逻辑
|
||||||
|
│ │ │ │ ├── company_info_service.go # 原QYGL逻辑
|
||||||
|
│ │ │ │ ├── data_query_service.go # 原IVYZ逻辑
|
||||||
|
│ │ │ │ └── app_system_service.go # 原YYSY逻辑
|
||||||
|
│ │ │ ├── application/
|
||||||
|
│ │ │ │ └── service/
|
||||||
|
│ │ │ │ └── data_orchestrator_service.go # 主协调器
|
||||||
|
│ │ │ ├── infrastructure/
|
||||||
|
│ │ │ │ └── external/
|
||||||
|
│ │ │ │ ├── westdex_client.go # 西部数据源客户端
|
||||||
|
│ │ │ │ ├── baidu_client.go # 百度API客户端
|
||||||
|
│ │ │ │ └── aliyun_client.go # 阿里云API客户端
|
||||||
|
│ │ │ └── interfaces/
|
||||||
|
│ │ │ └── http/
|
||||||
|
│ │ │ ├── risk_handler.go # /api/v1/data/risk-assessment
|
||||||
|
│ │ │ ├── credit_handler.go # /api/v1/data/credit-check
|
||||||
|
│ │ │ ├── company_handler.go # /api/v1/data/company-info
|
||||||
|
│ │ │ └── query_handler.go # /api/v1/data/query
|
||||||
|
│ │ └── ...
|
||||||
|
│ │
|
||||||
|
│ ├── security-domain/ # 安全域
|
||||||
|
│ │ └── security-service/
|
||||||
|
│ │ ├── cmd/
|
||||||
|
│ │ ├── api/grpc/
|
||||||
|
│ │ │ └── security.proto
|
||||||
|
│ │ ├── internal/
|
||||||
|
│ │ │ ├── domain/
|
||||||
|
│ │ │ │ └── service/
|
||||||
|
│ │ │ │ ├── encryption_service.go # 加密解密
|
||||||
|
│ │ │ │ ├── whitelist_service.go # 白名单管理
|
||||||
|
│ │ │ │ └── key_management_service.go # 密钥管理
|
||||||
|
│ │ │ └── ...
|
||||||
|
│ │ └── ...
|
||||||
|
│ │
|
||||||
|
│ ├── billing-domain/ # 计费域
|
||||||
|
│ │ └── billing-service/
|
||||||
|
│ │ ├── cmd/
|
||||||
|
│ │ ├── api/grpc/
|
||||||
|
│ │ │ └── billing.proto
|
||||||
|
│ │ ├── internal/
|
||||||
|
│ │ │ ├── domain/
|
||||||
|
│ │ │ │ ├── entity/
|
||||||
|
│ │ │ │ │ ├── wallet.go
|
||||||
|
│ │ │ │ │ ├── transaction.go
|
||||||
|
│ │ │ │ │ └── billing_record.go
|
||||||
|
│ │ │ │ └── service/
|
||||||
|
│ │ │ │ ├── wallet_service.go
|
||||||
|
│ │ │ │ ├── charging_service.go
|
||||||
|
│ │ │ │ └── billing_service.go
|
||||||
|
│ │ │ └── ...
|
||||||
|
│ │ └── ...
|
||||||
|
│ │
|
||||||
|
│ ├── product-domain/ # 产品域
|
||||||
|
│ │ └── product-service/
|
||||||
|
│ │ └── ... (类似结构)
|
||||||
|
│ │
|
||||||
|
│ ├── audit-domain/ # 审计域
|
||||||
|
│ │ └── audit-service/
|
||||||
|
│ │ └── ... (类似结构)
|
||||||
|
│ │
|
||||||
|
│ └── gateway-domain/ # 网关域
|
||||||
|
│ └── gateway-service/
|
||||||
|
│ ├── cmd/
|
||||||
|
│ │ └── server/main.go
|
||||||
|
│ ├── internal/
|
||||||
|
│ │ ├── middleware/ # 中间件
|
||||||
|
│ │ │ ├── auth.go
|
||||||
|
│ │ │ ├── rate_limit.go
|
||||||
|
│ │ │ └── audit.go
|
||||||
|
│ │ ├── router/ # 路由器
|
||||||
|
│ │ │ └── gateway_router.go
|
||||||
|
│ │ └── proxy/ # 代理
|
||||||
|
│ │ └── service_proxy.go
|
||||||
|
│ └── configs/
|
||||||
|
│ └── gateway.yaml
|
||||||
|
│
|
||||||
|
├── shared/ # 共享库
|
||||||
|
│ ├── pkg/ # 通用包
|
||||||
|
│ │ ├── crypto/ # 加密工具
|
||||||
|
│ │ ├── jwt/ # JWT工具
|
||||||
|
│ │ ├── response/ # 响应格式
|
||||||
|
│ │ ├── errors/ # 错误定义
|
||||||
|
│ │ ├── logger/ # 日志工具
|
||||||
|
│ │ └── utils/ # 工具函数
|
||||||
|
│ ├── events/ # 领域事件定义
|
||||||
|
│ │ ├── user_events.go
|
||||||
|
│ │ ├── billing_events.go
|
||||||
|
│ │ └── audit_events.go
|
||||||
|
│ └── proto/ # 公共protobuf定义
|
||||||
|
│ └── common.proto
|
||||||
|
│
|
||||||
|
├── infrastructure/ # 基础设施
|
||||||
|
│ ├── docker-compose.yaml # 本地开发环境
|
||||||
|
│ ├── kubernetes/ # K8s部署文件
|
||||||
|
│ │ ├── user-service.yaml
|
||||||
|
│ │ ├── data-service.yaml
|
||||||
|
│ │ └── gateway-service.yaml
|
||||||
|
│ ├── terraform/ # 基础设施即代码
|
||||||
|
│ └── monitoring/ # 监控配置
|
||||||
|
│ ├── prometheus/
|
||||||
|
│ └── grafana/
|
||||||
|
│
|
||||||
|
├── tools/ # 工具和脚本
|
||||||
|
│ ├── generate/ # 代码生成器
|
||||||
|
│ ├── migration/ # 数据迁移脚本
|
||||||
|
│ └── deploy/ # 部署脚本
|
||||||
|
│
|
||||||
|
├── docs/ # 项目文档
|
||||||
|
│ ├── architecture/ # 架构文档
|
||||||
|
│ ├── api/ # API文档
|
||||||
|
│ └── deployment/ # 部署文档
|
||||||
|
│
|
||||||
|
├── scripts/ # 构建和部署脚本
|
||||||
|
│ ├── build.sh
|
||||||
|
│ ├── deploy.sh
|
||||||
|
│ └── test.sh
|
||||||
|
│
|
||||||
|
├── .github/ # GitHub Actions
|
||||||
|
│ └── workflows/
|
||||||
|
│ ├── user-service.yml
|
||||||
|
│ ├── data-service.yml
|
||||||
|
│ └── gateway-service.yml
|
||||||
|
│
|
||||||
|
├── Makefile # 统一构建脚本
|
||||||
|
├── docker-compose.yml # 整体服务编排
|
||||||
|
└── README.md # 项目说明
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔄 从当前项目的迁移策略
|
||||||
|
|
||||||
|
### 当前项目映射关系
|
||||||
|
|
||||||
|
```
|
||||||
|
当前结构 → 新结构
|
||||||
|
|
||||||
|
apps/api/ → domains/data-domain/data-service/
|
||||||
|
├── FLXG/ → internal/domain/service/risk_assessment_service.go
|
||||||
|
├── JRZQ/ → internal/domain/service/credit_check_service.go
|
||||||
|
├── QYGL/ → internal/domain/service/company_info_service.go
|
||||||
|
├── IVYZ/ → internal/domain/service/data_query_service.go
|
||||||
|
└── YYSY/ → internal/domain/service/app_system_service.go
|
||||||
|
|
||||||
|
apps/user/ → domains/user-domain/user-service/
|
||||||
|
apps/sentinel/ → domains/product-domain/product-service/
|
||||||
|
apps/gateway/ → domains/gateway-domain/gateway-service/
|
||||||
|
apps/admin/ → domains/admin-domain/admin-service/
|
||||||
|
apps/mqs/ → domains/audit-domain/audit-service/
|
||||||
|
|
||||||
|
pkg/ → shared/pkg/
|
||||||
|
westDex/ → domains/data-domain/data-service/internal/infrastructure/external/
|
||||||
|
```
|
||||||
|
|
||||||
|
### 逐步迁移计划
|
||||||
|
|
||||||
|
#### 阶段 1: 创建新的目录结构 (1 周)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 创建新的目录结构
|
||||||
|
mkdir -p domains/{user,data,security,billing,product,audit,gateway}-domain
|
||||||
|
mkdir -p shared/{pkg,events,proto}
|
||||||
|
mkdir -p infrastructure/{kubernetes,terraform,monitoring}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 阶段 2: 迁移共享包 (1 周)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 迁移共享代码
|
||||||
|
mv pkg/ shared/pkg/
|
||||||
|
mv westDex/ domains/data-domain/data-service/internal/infrastructure/external/westdex/
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 阶段 3: 重构数据服务域 (2-3 周)
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 创建数据服务协调器
|
||||||
|
// domains/data-domain/data-service/internal/application/service/data_orchestrator.go
|
||||||
|
|
||||||
|
type DataOrchestrator struct {
|
||||||
|
riskService *RiskAssessmentService // 原FLXG
|
||||||
|
creditService *CreditCheckService // 原JRZQ
|
||||||
|
companyService *CompanyInfoService // 原QYGL
|
||||||
|
queryService *DataQueryService // 原IVYZ
|
||||||
|
appService *AppSystemService // 原YYSY
|
||||||
|
|
||||||
|
// 依赖的其他域服务
|
||||||
|
securityService SecurityServiceClient // gRPC客户端
|
||||||
|
userService UserServiceClient // gRPC客户端
|
||||||
|
productService ProductServiceClient // gRPC客户端
|
||||||
|
billingService BillingServiceClient // gRPC客户端
|
||||||
|
auditService AuditServiceClient // gRPC客户端
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DataOrchestrator) ProcessAPIRequest(ctx context.Context, req *APIRequest) (*APIResponse, error) {
|
||||||
|
// 之前分析的完整流程代码
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 阶段 4: 拆分其他域服务 (3-4 周)
|
||||||
|
|
||||||
|
逐个创建和迁移其他域服务
|
||||||
|
|
||||||
|
#### 阶段 5: 完善基础设施 (2 周)
|
||||||
|
|
||||||
|
添加监控、CI/CD、部署等
|
||||||
|
|
||||||
|
## 🚀 实际开发工作流
|
||||||
|
|
||||||
|
### 1. 单个服务开发
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd domains/user-domain/user-service
|
||||||
|
|
||||||
|
# 开发
|
||||||
|
go run cmd/server/main.go
|
||||||
|
|
||||||
|
# 测试
|
||||||
|
go test ./...
|
||||||
|
|
||||||
|
# 构建
|
||||||
|
go build -o bin/user-service cmd/server/main.go
|
||||||
|
|
||||||
|
# 部署
|
||||||
|
docker build -t user-service .
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 跨域开发
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 启动依赖服务
|
||||||
|
docker-compose up mysql redis kafka
|
||||||
|
|
||||||
|
# 启动相关服务
|
||||||
|
make start-user-service
|
||||||
|
make start-security-service
|
||||||
|
make start-data-service
|
||||||
|
|
||||||
|
# 集成测试
|
||||||
|
make test-integration
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 统一管理
|
||||||
|
|
||||||
|
```makefile
|
||||||
|
# Makefile
|
||||||
|
.PHONY: build-all start-all test-all
|
||||||
|
|
||||||
|
build-all:
|
||||||
|
cd domains/user-domain/user-service && go build -o ../../../bin/user-service cmd/server/main.go
|
||||||
|
cd domains/data-domain/data-service && go build -o ../../../bin/data-service cmd/server/main.go
|
||||||
|
cd domains/gateway-domain/gateway-service && go build -o ../../../bin/gateway-service cmd/server/main.go
|
||||||
|
|
||||||
|
start-all:
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
test-all:
|
||||||
|
cd domains/user-domain/user-service && go test ./...
|
||||||
|
cd domains/data-domain/data-service && go test ./...
|
||||||
|
cd domains/gateway-domain/gateway-service && go test ./...
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎯 总结
|
||||||
|
|
||||||
|
### 域与服务的关系:
|
||||||
|
|
||||||
|
- **域 = 业务边界** (逻辑概念)
|
||||||
|
- **服务 = 部署单元** (物理实现)
|
||||||
|
- **推荐:一域一服务** (对于你的项目规模)
|
||||||
|
|
||||||
|
### 目录架构核心原则:
|
||||||
|
|
||||||
|
1. **按域组织** - 顶层按业务域划分
|
||||||
|
2. **标准结构** - 每个服务内部结构一致
|
||||||
|
3. **清晰分层** - 领域层、应用层、基础设施层、接口层
|
||||||
|
4. **共享复用** - 公共代码放在 shared 目录
|
||||||
|
5. **独立部署** - 每个服务都可以独立构建部署
|
||||||
|
|
||||||
|
这种架构既保持了逻辑上的清晰性,又确保了技术实现的可操作性!
|
||||||
418
基于go-zero的完整微服务架构设计.md
Normal file
418
基于go-zero的完整微服务架构设计.md
Normal file
@@ -0,0 +1,418 @@
|
|||||||
|
# 基于 go-zero 的完整微服务架构设计
|
||||||
|
|
||||||
|
## 🏗️ 整体架构图
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────┐
|
||||||
|
│ External Access │
|
||||||
|
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
|
||||||
|
│ │Web App │ │Mobile │ │3rd API │ │Admin │ │
|
||||||
|
│ └────┬────┘ └────┬────┘ └────┬────┘ └────┬────┘ │
|
||||||
|
└───────│────────────│─────────────│─────────────│───────┘
|
||||||
|
│ │ │ │
|
||||||
|
└────────────┼─────────────┼─────────────┘
|
||||||
|
│ │
|
||||||
|
┌──────▼─────────────▼──────┐
|
||||||
|
│ Gateway API (HTTP) │ 🌐 HTTP入口
|
||||||
|
└──────┬────────────────────┘
|
||||||
|
│
|
||||||
|
┌───────────┼───────────┐
|
||||||
|
│ │ │
|
||||||
|
┌────▼────┐ ┌───▼───┐ ┌─────▼─────┐
|
||||||
|
│User RPC │ │Data │ │Admin HTTP │ 🔥 核心服务
|
||||||
|
│ │ │RPC │ │ │
|
||||||
|
└────┬────┘ └───┬───┘ └─────┬─────┘
|
||||||
|
│ │ │
|
||||||
|
┌────▼────┐ ┌───▼───┐ ┌─────▼─────┐
|
||||||
|
│Security │ │Billing│ │Audit MQS │ 🛡️ 支撑服务
|
||||||
|
│RPC │ │RPC │ │ │
|
||||||
|
└─────────┘ └───────┘ └───────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📁 完整目录结构
|
||||||
|
|
||||||
|
```
|
||||||
|
tianyuan-microservice/
|
||||||
|
├── api/ # HTTP API服务
|
||||||
|
│ ├── gateway/ # 🌐 API网关 (唯一HTTP入口)
|
||||||
|
│ │ ├── gateway.api # API定义
|
||||||
|
│ │ ├── gateway.go # 启动文件
|
||||||
|
│ │ ├── etc/
|
||||||
|
│ │ │ ├── gateway.yaml # 生产配置
|
||||||
|
│ │ │ └── gateway-dev.yaml # 开发配置
|
||||||
|
│ │ ├── internal/
|
||||||
|
│ │ │ ├── config/config.go # 配置结构
|
||||||
|
│ │ │ ├── handler/ # HTTP处理器
|
||||||
|
│ │ │ │ ├── auth/
|
||||||
|
│ │ │ │ │ ├── loginhandler.go
|
||||||
|
│ │ │ │ │ └── logouthandler.go
|
||||||
|
│ │ │ │ ├── data/
|
||||||
|
│ │ │ │ │ ├── riskhandler.go # /api/v1/data/risk
|
||||||
|
│ │ │ │ │ ├── credithandler.go # /api/v1/data/credit
|
||||||
|
│ │ │ │ │ └── companyhandler.go # /api/v1/data/company
|
||||||
|
│ │ │ │ ├── user/
|
||||||
|
│ │ │ │ │ └── userinfohandler.go
|
||||||
|
│ │ │ │ └── routes.go # 路由配置
|
||||||
|
│ │ │ ├── logic/ # 业务逻辑 (协调RPC调用)
|
||||||
|
│ │ │ │ ├── auth/
|
||||||
|
│ │ │ │ │ └── loginlogic.go
|
||||||
|
│ │ │ │ ├── data/
|
||||||
|
│ │ │ │ │ ├── risklogic.go # 协调data-rpc
|
||||||
|
│ │ │ │ │ ├── creditlogic.go
|
||||||
|
│ │ │ │ │ └── companylogic.go
|
||||||
|
│ │ │ │ └── user/
|
||||||
|
│ │ │ │ └── userinfologic.go
|
||||||
|
│ │ │ ├── middleware/ # 中间件
|
||||||
|
│ │ │ │ ├── authinterceptor.go # JWT认证
|
||||||
|
│ │ │ │ ├── ratelimit.go # 限流
|
||||||
|
│ │ │ │ └── audit.go # 审计
|
||||||
|
│ │ │ ├── svc/servicecontext.go # 服务上下文 (包含所有RPC客户端)
|
||||||
|
│ │ │ └── types/types.go # 请求响应类型
|
||||||
|
│ │ ├── Dockerfile
|
||||||
|
│ │ └── k8s/
|
||||||
|
│ │ └── deployment.yaml
|
||||||
|
│ │
|
||||||
|
│ └── admin/ # 🏢 管理后台 (独立HTTP服务)
|
||||||
|
│ ├── admin.api
|
||||||
|
│ ├── admin.go
|
||||||
|
│ ├── etc/admin.yaml
|
||||||
|
│ ├── internal/
|
||||||
|
│ │ ├── handler/
|
||||||
|
│ │ │ ├── product/
|
||||||
|
│ │ │ │ ├── createproducthandler.go
|
||||||
|
│ │ │ │ └── listproducthandler.go
|
||||||
|
│ │ │ ├── review/
|
||||||
|
│ │ │ │ └── reviewenterprisehandler.go
|
||||||
|
│ │ │ └── routes.go
|
||||||
|
│ │ ├── logic/
|
||||||
|
│ │ ├── svc/servicecontext.go # 包含RPC客户端
|
||||||
|
│ │ └── types/types.go
|
||||||
|
│ ├── Dockerfile
|
||||||
|
│ └── k8s/deployment.yaml
|
||||||
|
│
|
||||||
|
├── rpc/ # RPC微服务集群
|
||||||
|
│ ├── user/ # 👥 用户域RPC服务
|
||||||
|
│ │ ├── user.proto # gRPC定义
|
||||||
|
│ │ ├── user.go # 启动文件
|
||||||
|
│ │ ├── etc/user.yaml # 配置
|
||||||
|
│ │ ├── internal/
|
||||||
|
│ │ │ ├── config/config.go
|
||||||
|
│ │ │ ├── logic/ # 业务逻辑
|
||||||
|
│ │ │ │ ├── auth/
|
||||||
|
│ │ │ │ │ ├── loginlogic.go
|
||||||
|
│ │ │ │ │ └── validatetokenlogic.go
|
||||||
|
│ │ │ │ ├── user/
|
||||||
|
│ │ │ │ │ ├── getuserinfologic.go
|
||||||
|
│ │ │ │ │ └── updateuserlogic.go
|
||||||
|
│ │ │ │ └── enterprise/
|
||||||
|
│ │ │ │ ├── validateenterpriselogic.go
|
||||||
|
│ │ │ │ └── getenterpriselogic.go
|
||||||
|
│ │ │ ├── server/userserver.go # gRPC服务实现
|
||||||
|
│ │ │ ├── svc/servicecontext.go # 服务上下文
|
||||||
|
│ │ │ └── model/ # 数据模型
|
||||||
|
│ │ │ ├── usermodel.go # 用户表模型
|
||||||
|
│ │ │ ├── enterprisemodel.go # 企业表模型
|
||||||
|
│ │ │ └── vars.go # 常量定义
|
||||||
|
│ │ ├── pb/ # 生成的protobuf文件
|
||||||
|
│ │ │ ├── user.pb.go
|
||||||
|
│ │ │ └── user_grpc.pb.go
|
||||||
|
│ │ ├── Dockerfile
|
||||||
|
│ │ └── k8s/deployment.yaml
|
||||||
|
│ │
|
||||||
|
│ ├── data/ # 📊 数据服务域RPC服务 (核心协调者)
|
||||||
|
│ │ ├── data.proto
|
||||||
|
│ │ ├── data.go
|
||||||
|
│ │ ├── etc/data.yaml
|
||||||
|
│ │ ├── internal/
|
||||||
|
│ │ │ ├── logic/
|
||||||
|
│ │ │ │ ├── orchestrator/
|
||||||
|
│ │ │ │ │ └── dataorchestratorlogic.go # 🔥 核心协调逻辑
|
||||||
|
│ │ │ │ ├── risk/ # 原FLXG业务
|
||||||
|
│ │ │ │ │ ├── riskassessmentlogic.go
|
||||||
|
│ │ │ │ │ └── riskreportlogic.go
|
||||||
|
│ │ │ │ ├── credit/ # 原JRZQ业务
|
||||||
|
│ │ │ │ │ ├── creditchecklogic.go
|
||||||
|
│ │ │ │ │ └── credithistorylogic.go
|
||||||
|
│ │ │ │ ├── company/ # 原QYGL业务
|
||||||
|
│ │ │ │ │ ├── companyinfologic.go
|
||||||
|
│ │ │ │ │ └── companysearchlogic.go
|
||||||
|
│ │ │ │ └── query/ # 原IVYZ业务
|
||||||
|
│ │ │ │ ├── dataquerylogic.go
|
||||||
|
│ │ │ │ └── batchquerylogic.go
|
||||||
|
│ │ │ ├── server/dataserver.go
|
||||||
|
│ │ │ ├── svc/servicecontext.go # 包含其他RPC客户端
|
||||||
|
│ │ │ └── external/ # 外部API客户端
|
||||||
|
│ │ │ ├── westdex/westdexclient.go # 西部数据源
|
||||||
|
│ │ │ ├── baidu/baiduclient.go # 百度API
|
||||||
|
│ │ │ └── aliyun/aliyunclient.go # 阿里云API
|
||||||
|
│ │ ├── pb/
|
||||||
|
│ │ ├── Dockerfile
|
||||||
|
│ │ └── k8s/deployment.yaml
|
||||||
|
│ │
|
||||||
|
│ ├── security/ # 🔐 安全域RPC服务
|
||||||
|
│ │ ├── security.proto
|
||||||
|
│ │ ├── security.go
|
||||||
|
│ │ ├── etc/security.yaml
|
||||||
|
│ │ ├── internal/
|
||||||
|
│ │ │ ├── logic/
|
||||||
|
│ │ │ │ ├── encryption/
|
||||||
|
│ │ │ │ │ ├── encryptlogic.go
|
||||||
|
│ │ │ │ │ └── decryptlogic.go
|
||||||
|
│ │ │ │ ├── whitelist/
|
||||||
|
│ │ │ │ │ ├── checkwhitelistlogic.go
|
||||||
|
│ │ │ │ │ └── addwhitelistlogic.go
|
||||||
|
│ │ │ │ └── secret/
|
||||||
|
│ │ │ │ ├── generatesecretlogic.go
|
||||||
|
│ │ │ │ └── validatesecretlogic.go
|
||||||
|
│ │ │ ├── server/securityserver.go
|
||||||
|
│ │ │ ├── svc/servicecontext.go
|
||||||
|
│ │ │ └── model/
|
||||||
|
│ │ │ ├── whitelistmodel.go # IP白名单表
|
||||||
|
│ │ │ └── secretkeymodel.go # 密钥表
|
||||||
|
│ │ ├── pb/
|
||||||
|
│ │ ├── Dockerfile
|
||||||
|
│ │ └── k8s/deployment.yaml
|
||||||
|
│ │
|
||||||
|
│ ├── billing/ # 💰 计费域RPC服务
|
||||||
|
│ │ ├── billing.proto
|
||||||
|
│ │ ├── billing.go
|
||||||
|
│ │ ├── etc/billing.yaml
|
||||||
|
│ │ ├── internal/
|
||||||
|
│ │ │ ├── logic/
|
||||||
|
│ │ │ │ ├── wallet/
|
||||||
|
│ │ │ │ │ ├── getbalancelogic.go
|
||||||
|
│ │ │ │ │ ├── rechargelogic.go
|
||||||
|
│ │ │ │ │ └── freezelogic.go
|
||||||
|
│ │ │ │ ├── charge/
|
||||||
|
│ │ │ │ │ ├── chargelogic.go # 扣费逻辑
|
||||||
|
│ │ │ │ │ ├── refundlogic.go # 退费逻辑
|
||||||
|
│ │ │ │ │ └── calculatelogic.go # 计费计算
|
||||||
|
│ │ │ │ └── order/
|
||||||
|
│ │ │ │ ├── createorderlogic.go
|
||||||
|
│ │ │ │ └── queryorderlogic.go
|
||||||
|
│ │ │ ├── server/billingserver.go
|
||||||
|
│ │ │ ├── svc/servicecontext.go
|
||||||
|
│ │ │ └── model/
|
||||||
|
│ │ │ ├── walletmodel.go # 钱包表
|
||||||
|
│ │ │ ├── transactionmodel.go # 交易记录表
|
||||||
|
│ │ │ └── ordermodel.go # 订单表
|
||||||
|
│ │ ├── pb/
|
||||||
|
│ │ ├── Dockerfile
|
||||||
|
│ │ └── k8s/deployment.yaml
|
||||||
|
│ │
|
||||||
|
│ ├── product/ # 🛍️ 产品域RPC服务
|
||||||
|
│ │ ├── product.proto
|
||||||
|
│ │ ├── product.go
|
||||||
|
│ │ ├── etc/product.yaml
|
||||||
|
│ │ ├── internal/
|
||||||
|
│ │ │ ├── logic/
|
||||||
|
│ │ │ │ ├── product/
|
||||||
|
│ │ │ │ │ ├── createproductlogic.go
|
||||||
|
│ │ │ │ │ ├── getproductlogic.go
|
||||||
|
│ │ │ │ │ └── updateproductlogic.go
|
||||||
|
│ │ │ │ ├── access/
|
||||||
|
│ │ │ │ │ ├── checkproductaccesslogic.go
|
||||||
|
│ │ │ │ │ └── grantaccesslogic.go
|
||||||
|
│ │ │ │ └── subscription/
|
||||||
|
│ │ │ │ ├── subscribelogic.go
|
||||||
|
│ │ │ │ └── unsubscribelogic.go
|
||||||
|
│ │ │ ├── server/productserver.go
|
||||||
|
│ │ │ ├── svc/servicecontext.go
|
||||||
|
│ │ │ └── model/
|
||||||
|
│ │ │ ├── productmodel.go # 产品表
|
||||||
|
│ │ │ ├── userproductmodel.go # 用户产品关系表
|
||||||
|
│ │ │ └── productaccessmodel.go # 产品权限表
|
||||||
|
│ │ ├── pb/
|
||||||
|
│ │ ├── Dockerfile
|
||||||
|
│ │ └── k8s/deployment.yaml
|
||||||
|
│ │
|
||||||
|
│ └── audit/ # 📝 审计域RPC服务
|
||||||
|
│ ├── audit.proto
|
||||||
|
│ ├── audit.go
|
||||||
|
│ ├── etc/audit.yaml
|
||||||
|
│ ├── internal/
|
||||||
|
│ │ ├── logic/
|
||||||
|
│ │ │ ├── apilog/
|
||||||
|
│ │ │ │ ├── recordapiloglogic.go
|
||||||
|
│ │ │ │ └── queryapiloglogic.go
|
||||||
|
│ │ │ ├── operation/
|
||||||
|
│ │ │ │ ├── recordoperationlogic.go
|
||||||
|
│ │ │ │ └── queryoperationlogic.go
|
||||||
|
│ │ │ └── statistics/
|
||||||
|
│ │ │ ├── apistatslogic.go
|
||||||
|
│ │ │ └── usagestatslogic.go
|
||||||
|
│ │ ├── server/auditserver.go
|
||||||
|
│ │ ├── svc/servicecontext.go
|
||||||
|
│ │ └── model/
|
||||||
|
│ │ ├── apilogmodel.go # API调用日志表
|
||||||
|
│ │ ├── operationlogmodel.go # 操作日志表
|
||||||
|
│ │ └── statisticsmodel.go # 统计数据表
|
||||||
|
│ ├── pb/
|
||||||
|
│ ├── Dockerfile
|
||||||
|
│ └── k8s/deployment.yaml
|
||||||
|
│
|
||||||
|
├── mqs/ # 📮 消息队列服务
|
||||||
|
│ ├── consumer/ # 消息消费者
|
||||||
|
│ │ ├── audit/ # 审计消息消费
|
||||||
|
│ │ │ ├── auditconsumer.go
|
||||||
|
│ │ │ └── etc/auditconsumer.yaml
|
||||||
|
│ │ ├── billing/ # 计费消息消费
|
||||||
|
│ │ │ ├── billingconsumer.go
|
||||||
|
│ │ │ └── etc/billingconsumer.yaml
|
||||||
|
│ │ └── notification/ # 通知消息消费
|
||||||
|
│ │ ├── notificationconsumer.go
|
||||||
|
│ │ └── etc/notificationconsumer.yaml
|
||||||
|
│ └── producer/ # 消息生产者 (在各RPC服务中)
|
||||||
|
│
|
||||||
|
├── shared/ # 🔧 共享库
|
||||||
|
│ ├── pkg/
|
||||||
|
│ │ ├── crypto/ # 加密工具
|
||||||
|
│ │ │ ├── aes.go
|
||||||
|
│ │ │ └── rsa.go
|
||||||
|
│ │ ├── jwt/ # JWT工具
|
||||||
|
│ │ │ └── jwt.go
|
||||||
|
│ │ ├── response/ # 统一响应格式
|
||||||
|
│ │ │ └── response.go
|
||||||
|
│ │ ├── errors/ # 错误定义
|
||||||
|
│ │ │ └── errors.go
|
||||||
|
│ │ ├── utils/ # 工具函数
|
||||||
|
│ │ │ ├── strings.go
|
||||||
|
│ │ │ └── time.go
|
||||||
|
│ │ └── middleware/ # 公共中间件
|
||||||
|
│ │ ├── recovery.go
|
||||||
|
│ │ └── prometheus.go
|
||||||
|
│ ├── model/ # 公共数据模型
|
||||||
|
│ │ ├── common.go # 通用字段
|
||||||
|
│ │ └── pagination.go # 分页模型
|
||||||
|
│ └── proto/ # 公共protobuf定义
|
||||||
|
│ └── common.proto
|
||||||
|
│
|
||||||
|
├── deploy/ # 🚀 部署相关
|
||||||
|
│ ├── docker/
|
||||||
|
│ │ ├── docker-compose.dev.yml # 开发环境
|
||||||
|
│ │ ├── docker-compose.prod.yml # 生产环境
|
||||||
|
│ │ └── nginx/ # Nginx配置
|
||||||
|
│ │ └── nginx.conf
|
||||||
|
│ ├── k8s/ # Kubernetes部署
|
||||||
|
│ │ ├── namespace.yaml
|
||||||
|
│ │ ├── configmap.yaml
|
||||||
|
│ │ ├── mysql.yaml
|
||||||
|
│ │ ├── redis.yaml
|
||||||
|
│ │ ├── kafka.yaml
|
||||||
|
│ │ ├── etcd.yaml
|
||||||
|
│ │ └── ingress.yaml
|
||||||
|
│ ├── terraform/ # 基础设施即代码
|
||||||
|
│ │ ├── main.tf
|
||||||
|
│ │ ├── variables.tf
|
||||||
|
│ │ └── outputs.tf
|
||||||
|
│ └── scripts/ # 部署脚本
|
||||||
|
│ ├── build.sh # 构建脚本
|
||||||
|
│ ├── deploy.sh # 部署脚本
|
||||||
|
│ └── migrate.sh # 数据库迁移脚本
|
||||||
|
│
|
||||||
|
├── tools/ # 🛠️ 开发工具
|
||||||
|
│ ├── goctl/ # go-zero代码生成工具配置
|
||||||
|
│ │ ├── api.tpl # API模板
|
||||||
|
│ │ └── rpc.tpl # RPC模板
|
||||||
|
│ ├── migrate/ # 数据库迁移工具
|
||||||
|
│ │ ├── migrations/
|
||||||
|
│ │ │ ├── 001_create_users.sql
|
||||||
|
│ │ │ ├── 002_create_enterprises.sql
|
||||||
|
│ │ │ ├── 003_create_products.sql
|
||||||
|
│ │ │ ├── 004_create_wallets.sql
|
||||||
|
│ │ │ └── 005_create_audit_logs.sql
|
||||||
|
│ │ └── migrate.go
|
||||||
|
│ └── generate/ # 代码生成脚本
|
||||||
|
│ ├── gen-api.sh # 生成API代码
|
||||||
|
│ ├── gen-rpc.sh # 生成RPC代码
|
||||||
|
│ └── gen-model.sh # 生成Model代码
|
||||||
|
│
|
||||||
|
├── docs/ # 📚 项目文档
|
||||||
|
│ ├── api/ # API文档
|
||||||
|
│ │ ├── gateway.md
|
||||||
|
│ │ └── admin.md
|
||||||
|
│ ├── rpc/ # RPC服务文档
|
||||||
|
│ │ ├── user.md
|
||||||
|
│ │ ├── data.md
|
||||||
|
│ │ └── billing.md
|
||||||
|
│ ├── database/ # 数据库设计文档
|
||||||
|
│ │ ├── schema.md
|
||||||
|
│ │ └── erd.md
|
||||||
|
│ └── deployment/ # 部署文档
|
||||||
|
│ ├── k8s.md
|
||||||
|
│ └── docker.md
|
||||||
|
│
|
||||||
|
├── .github/ # 🔄 CI/CD
|
||||||
|
│ └── workflows/
|
||||||
|
│ ├── api-gateway.yml
|
||||||
|
│ ├── user-rpc.yml
|
||||||
|
│ ├── data-rpc.yml
|
||||||
|
│ └── deploy.yml
|
||||||
|
│
|
||||||
|
├── go.work # Go工作区配置
|
||||||
|
├── Makefile # 统一构建脚本
|
||||||
|
├── README.md # 项目说明
|
||||||
|
└── .gitignore # Git忽略文件
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎯 服务分类与职责
|
||||||
|
|
||||||
|
### HTTP 服务 (对外接口)
|
||||||
|
|
||||||
|
- **gateway**: 唯一 HTTP 入口,负责路由和协调
|
||||||
|
- **admin**: 管理后台,企业管理、产品管理、审核等
|
||||||
|
|
||||||
|
### RPC 服务 (内部通信)
|
||||||
|
|
||||||
|
- **user**: 用户、企业、认证管理
|
||||||
|
- **data**: 数据服务协调器 + 原有业务逻辑
|
||||||
|
- **security**: 加密解密、白名单、密钥管理
|
||||||
|
- **billing**: 钱包、计费、订单管理
|
||||||
|
- **product**: 产品、权限、订阅管理
|
||||||
|
- **audit**: 日志记录、统计分析
|
||||||
|
|
||||||
|
### MQS 服务 (异步处理)
|
||||||
|
|
||||||
|
- **audit-consumer**: 异步处理审计日志
|
||||||
|
- **billing-consumer**: 异步处理计费通知
|
||||||
|
- **notification-consumer**: 异步处理消息通知
|
||||||
|
|
||||||
|
## 💾 数据库设计
|
||||||
|
|
||||||
|
### 按域分库
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
databases:
|
||||||
|
user_db: # 用户域数据库
|
||||||
|
tables:
|
||||||
|
- users
|
||||||
|
- enterprises
|
||||||
|
- user_tokens
|
||||||
|
|
||||||
|
security_db: # 安全域数据库
|
||||||
|
tables:
|
||||||
|
- ip_whitelist
|
||||||
|
- secret_keys
|
||||||
|
- encrypt_records
|
||||||
|
|
||||||
|
billing_db: # 计费域数据库
|
||||||
|
tables:
|
||||||
|
- wallets
|
||||||
|
- transactions
|
||||||
|
- orders
|
||||||
|
- billing_records
|
||||||
|
|
||||||
|
product_db: # 产品域数据库
|
||||||
|
tables:
|
||||||
|
- products
|
||||||
|
- user_products
|
||||||
|
- product_access
|
||||||
|
|
||||||
|
audit_db: # 审计域数据库
|
||||||
|
tables:
|
||||||
|
- api_logs
|
||||||
|
- operation_logs
|
||||||
|
- statistics
|
||||||
|
```
|
||||||
|
|
||||||
|
接下来我会详细说明每个部分的具体实现...
|
||||||
229
校验体系设计优化.md
Normal file
229
校验体系设计优化.md
Normal file
@@ -0,0 +1,229 @@
|
|||||||
|
# 校验体系设计优化
|
||||||
|
|
||||||
|
## 1. 校验层次重新设计
|
||||||
|
|
||||||
|
### Handler 层(client-api)
|
||||||
|
|
||||||
|
```go
|
||||||
|
// types.go - 只做格式校验
|
||||||
|
type GetProductListReq struct {
|
||||||
|
CategoryId int64 `form:"category_id" validate:"min=1"` // 格式校验:必须大于0
|
||||||
|
PageNum int64 `form:"page_num" validate:"min=1,max=1000"` // 格式校验:分页范围
|
||||||
|
PageSize int64 `form:"page_size" validate:"min=1,max=100"` // 格式校验:每页数量
|
||||||
|
Keyword string `form:"keyword" validate:"max=50"` // 格式校验:关键词长度
|
||||||
|
}
|
||||||
|
|
||||||
|
// handler.go - 只做参数绑定和格式校验
|
||||||
|
func (l *GetProductListHandler) GetProductList(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req types.GetProductListReq
|
||||||
|
|
||||||
|
// 1. 参数绑定
|
||||||
|
if err := httpx.Parse(r, &req); err != nil {
|
||||||
|
response.ParamErrorResponse(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 格式校验(只校验格式,不校验业务)
|
||||||
|
if err := validator.Validate(&req); err != nil {
|
||||||
|
response.ParamErrorResponse(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 调用Logic层
|
||||||
|
resp, err := l.svcCtx.GetProductListLogic(l.ctx, &req)
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### RPC 层(product-rpc)
|
||||||
|
|
||||||
|
```go
|
||||||
|
// product.proto - RPC接口定义
|
||||||
|
message GetProductListReq {
|
||||||
|
int64 category_id = 1;
|
||||||
|
int64 page_num = 2;
|
||||||
|
int64 page_size = 3;
|
||||||
|
string keyword = 4;
|
||||||
|
int64 user_id = 5; // 业务相关:用户ID
|
||||||
|
string user_role = 6; // 业务相关:用户角色
|
||||||
|
}
|
||||||
|
|
||||||
|
// logic.go - 业务校验
|
||||||
|
func (l *GetProductListLogic) GetProductList(req *product.GetProductListReq) (*product.GetProductListResp, error) {
|
||||||
|
// 1. 业务校验(在相关业务域内)
|
||||||
|
if err := l.validateBusinessRules(req); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 业务逻辑处理
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *GetProductListLogic) validateBusinessRules(req *product.GetProductListReq) error {
|
||||||
|
// 业务规则校验:用户是否有权限查看该分类
|
||||||
|
if !l.svcCtx.UserModel.HasCategoryPermission(req.UserId, req.CategoryId) {
|
||||||
|
return errors.New("用户无权限查看该分类")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 业务规则校验:分类是否存在且启用
|
||||||
|
category, err := l.svcCtx.CategoryModel.FindOne(req.CategoryId)
|
||||||
|
if err != nil || category.Status != 1 {
|
||||||
|
return errors.New("分类不存在或已禁用")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. 参数传递和转换
|
||||||
|
|
||||||
|
### Logic 层的作用
|
||||||
|
|
||||||
|
```go
|
||||||
|
// client/internal/logic/product/getproductlistlogic.go
|
||||||
|
func (l *GetProductListLogic) GetProductList(req *types.GetProductListReq) (*types.GetProductListResp, error) {
|
||||||
|
// 1. 获取用户信息(从JWT中解析)
|
||||||
|
userInfo := l.ctx.Value("userInfo").(auth.UserInfo)
|
||||||
|
|
||||||
|
// 2. 转换为RPC请求(添加业务上下文)
|
||||||
|
rpcReq := &product.GetProductListReq{
|
||||||
|
CategoryId: req.CategoryId,
|
||||||
|
PageNum: req.PageNum,
|
||||||
|
PageSize: req.PageSize,
|
||||||
|
Keyword: req.Keyword,
|
||||||
|
UserId: userInfo.UserId, // 添加业务上下文
|
||||||
|
UserRole: userInfo.Role, // 添加业务上下文
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 调用RPC服务
|
||||||
|
rpcResp, err := l.svcCtx.ProductRpc.GetProductList(l.ctx, rpcReq)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 转换响应格式
|
||||||
|
return &types.GetProductListResp{
|
||||||
|
List: convertToApiFormat(rpcResp.List),
|
||||||
|
Total: rpcResp.Total,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. 校验规则组织优化
|
||||||
|
|
||||||
|
### 目录结构调整
|
||||||
|
|
||||||
|
```
|
||||||
|
shared/
|
||||||
|
├── validator/ # 通用格式校验器
|
||||||
|
│ ├── validator.go # 校验器初始化
|
||||||
|
│ ├── rules.go # 通用校验规则
|
||||||
|
│ └── messages.go # 错误消息
|
||||||
|
├── errcode/ # 错误码定义
|
||||||
|
│ ├── api_errors.go # API层错误
|
||||||
|
│ └── business_errors.go # 业务层错误
|
||||||
|
└── utils/ # 工具函数
|
||||||
|
└── converter.go # 数据转换
|
||||||
|
|
||||||
|
domains/
|
||||||
|
├── product/
|
||||||
|
│ ├── rpc/
|
||||||
|
│ │ └── internal/
|
||||||
|
│ │ └── logic/
|
||||||
|
│ │ └── validator/ # 产品域业务校验
|
||||||
|
│ │ ├── product_validator.go
|
||||||
|
│ │ └── category_validator.go
|
||||||
|
├── user/
|
||||||
|
│ └── rpc/
|
||||||
|
│ └── internal/
|
||||||
|
│ └── logic/
|
||||||
|
│ └── validator/ # 用户域业务校验
|
||||||
|
│ ├── user_validator.go
|
||||||
|
│ └── auth_validator.go
|
||||||
|
```
|
||||||
|
|
||||||
|
### 业务校验器实现
|
||||||
|
|
||||||
|
```go
|
||||||
|
// domains/product/rpc/internal/logic/validator/product_validator.go
|
||||||
|
package validator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"tianyuan/domains/product/rpc/internal/svc"
|
||||||
|
"tianyuan/domains/product/rpc/product"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProductValidator struct {
|
||||||
|
svcCtx *svc.ServiceContext
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewProductValidator(svcCtx *svc.ServiceContext) *ProductValidator {
|
||||||
|
return &ProductValidator{
|
||||||
|
svcCtx: svcCtx,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 校验用户是否有权限查看产品列表
|
||||||
|
func (v *ProductValidator) ValidateGetProductListPermission(ctx context.Context, req *product.GetProductListReq) error {
|
||||||
|
// 检查用户权限
|
||||||
|
hasPermission, err := v.svcCtx.UserRpc.CheckPermission(ctx, &user.CheckPermissionReq{
|
||||||
|
UserId: req.UserId,
|
||||||
|
Permission: "product:list",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !hasPermission.HasPermission {
|
||||||
|
return errors.New("用户无权限查看产品列表")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 校验分类相关业务规则
|
||||||
|
func (v *ProductValidator) ValidateCategoryAccess(ctx context.Context, req *product.GetProductListReq) error {
|
||||||
|
// 检查分类是否存在且用户有访问权限
|
||||||
|
// ...
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. 关于 httpx.Parse 的说明
|
||||||
|
|
||||||
|
`httpx.Parse` 是 go-zero 提供的参数绑定函数,它会:
|
||||||
|
|
||||||
|
1. **自动绑定参数**:根据 struct tag 从 HTTP 请求中解析参数
|
||||||
|
2. **支持多种来源**:query 参数、form 数据、JSON body 等
|
||||||
|
3. **类型转换**:自动进行基础类型转换
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 示例:httpx.Parse 的使用
|
||||||
|
type GetProductListReq struct {
|
||||||
|
CategoryId int64 `form:"category_id"` // 从form或query中获取
|
||||||
|
PageNum int64 `form:"page_num"`
|
||||||
|
PageSize int64 `form:"page_size"`
|
||||||
|
Keyword string `form:"keyword"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func Handler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req GetProductListReq
|
||||||
|
|
||||||
|
// 这一行会自动从 r *http.Request 中解析参数到 req 结构体
|
||||||
|
if err := httpx.Parse(r, &req); err != nil {
|
||||||
|
// 参数解析失败
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 此时 req 已经包含了解析后的参数值
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. 最佳实践总结
|
||||||
|
|
||||||
|
1. **Handler 层**:只做格式校验,使用 validator tag
|
||||||
|
2. **Logic 层**:参数转换和聚合,添加业务上下文
|
||||||
|
3. **RPC 层**:业务规则校验,放在对应的业务域内
|
||||||
|
4. **校验器分离**:格式校验器放 shared,业务校验器放各自域内
|
||||||
|
5. **参数绑定**:使用 go-zero 的 httpx.Parse 自动绑定
|
||||||
414
网关域架构与跨域调用实现.md
Normal file
414
网关域架构与跨域调用实现.md
Normal file
@@ -0,0 +1,414 @@
|
|||||||
|
# 网关域架构与跨域调用实现详解
|
||||||
|
|
||||||
|
## 🌐 网关域的职责与架构
|
||||||
|
|
||||||
|
### 网关域的核心职责
|
||||||
|
|
||||||
|
```
|
||||||
|
网关域 (Gateway Domain) = API网关 + 路由管理 + 协议转换
|
||||||
|
```
|
||||||
|
|
||||||
|
**主要功能:**
|
||||||
|
|
||||||
|
1. **统一入口** - 所有外部请求的唯一入口点
|
||||||
|
2. **路由分发** - 根据路径将请求分发到对应的域服务
|
||||||
|
3. **协议转换** - HTTP ↔ gRPC 转换
|
||||||
|
4. **横切关注点** - 认证、限流、监控、日志
|
||||||
|
5. **服务发现** - 动态发现后端服务
|
||||||
|
|
||||||
|
### 网关域架构图
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ Gateway Domain │
|
||||||
|
│ ┌─────────────────────────────────────────────────────────┐│
|
||||||
|
│ │ Gateway Service ││
|
||||||
|
│ │ ││
|
||||||
|
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ││
|
||||||
|
│ │ │ HTTP │ │ Router │ │ gRPC │ ││
|
||||||
|
│ │ │ Handlers │ │ Engine │ │ Clients │ ││
|
||||||
|
│ │ └─────────────┘ └─────────────┘ └─────────────┘ ││
|
||||||
|
│ │ ││
|
||||||
|
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ││
|
||||||
|
│ │ │ Auth │ │ Rate Limit │ │ Monitor │ ││
|
||||||
|
│ │ │ Middleware │ │ Middleware │ │ Middleware │ ││
|
||||||
|
│ │ └─────────────┘ └─────────────┘ └─────────────┘ ││
|
||||||
|
│ └─────────────────────────────────────────────────────────┘│
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
│ │ │
|
||||||
|
▼ ▼ ▼
|
||||||
|
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
||||||
|
│ User Domain │ │ Data Domain │ │ Billing Domain │
|
||||||
|
│ (User Service) │ │ (Data Service) │ │(Billing Service)│
|
||||||
|
└─────────────────┘ └─────────────────┘ └─────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🏗️ 网关域的目录结构
|
||||||
|
|
||||||
|
```
|
||||||
|
domains/gateway-domain/
|
||||||
|
└── gateway-service/
|
||||||
|
├── cmd/
|
||||||
|
│ └── server/
|
||||||
|
│ └── main.go # 网关服务启动入口
|
||||||
|
│
|
||||||
|
├── api/
|
||||||
|
│ └── http/
|
||||||
|
│ └── gateway.api # 网关API定义
|
||||||
|
│
|
||||||
|
├── internal/
|
||||||
|
│ ├── application/ # 应用层
|
||||||
|
│ │ └── service/
|
||||||
|
│ │ └── gateway_orchestrator.go # 🔥 核心协调器
|
||||||
|
│ │
|
||||||
|
│ ├── domain/ # 领域层
|
||||||
|
│ │ ├── entity/
|
||||||
|
│ │ │ ├── route.go # 路由实体
|
||||||
|
│ │ │ └── service_endpoint.go # 服务端点
|
||||||
|
│ │ └── service/
|
||||||
|
│ │ ├── router_service.go # 路由服务
|
||||||
|
│ │ └── discovery_service.go # 服务发现
|
||||||
|
│ │
|
||||||
|
│ ├── infrastructure/ # 基础设施层
|
||||||
|
│ │ ├── client/ # gRPC客户端
|
||||||
|
│ │ │ ├── user_client.go
|
||||||
|
│ │ │ ├── data_client.go
|
||||||
|
│ │ │ ├── security_client.go
|
||||||
|
│ │ │ ├── billing_client.go
|
||||||
|
│ │ │ └── product_client.go
|
||||||
|
│ │ ├── discovery/ # 服务发现
|
||||||
|
│ │ │ └── etcd_discovery.go
|
||||||
|
│ │ └── config/
|
||||||
|
│ │ └── service_config.go # 服务配置
|
||||||
|
│ │
|
||||||
|
│ └── interfaces/ # 接口适配器层
|
||||||
|
│ ├── http/ # HTTP处理器
|
||||||
|
│ │ ├── data_handler.go # 数据服务API处理
|
||||||
|
│ │ ├── user_handler.go # 用户服务API处理
|
||||||
|
│ │ └── billing_handler.go # 计费服务API处理
|
||||||
|
│ ├── middleware/ # 中间件
|
||||||
|
│ │ ├── auth_middleware.go # 认证中间件
|
||||||
|
│ │ ├── rate_limit_middleware.go # 限流中间件
|
||||||
|
│ │ ├── audit_middleware.go # 审计中间件
|
||||||
|
│ │ └── cors_middleware.go # CORS中间件
|
||||||
|
│ └── router/
|
||||||
|
│ └── gateway_router.go # 路由配置
|
||||||
|
│
|
||||||
|
├── configs/
|
||||||
|
│ ├── gateway.yaml # 网关配置
|
||||||
|
│ └── routes.yaml # 路由配置
|
||||||
|
│
|
||||||
|
└── deployments/
|
||||||
|
├── Dockerfile
|
||||||
|
└── k8s/
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎯 核心实现:跨域调用 Logic 在哪里
|
||||||
|
|
||||||
|
### 1. 网关层的协调逻辑 (Gateway Orchestrator)
|
||||||
|
|
||||||
|
```go
|
||||||
|
// domains/gateway-domain/gateway-service/internal/application/service/gateway_orchestrator.go
|
||||||
|
|
||||||
|
type GatewayOrchestrator struct {
|
||||||
|
// gRPC客户端 - 调用各个域服务
|
||||||
|
userClient user.UserServiceClient
|
||||||
|
dataClient data.DataServiceClient
|
||||||
|
securityClient security.SecurityServiceClient
|
||||||
|
billingClient billing.BillingServiceClient
|
||||||
|
productClient product.ProductServiceClient
|
||||||
|
auditClient audit.AuditServiceClient
|
||||||
|
|
||||||
|
// 网关自身的服务
|
||||||
|
routerService *RouterService
|
||||||
|
discoveryService *DiscoveryService
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🔥 这里实现跨域调用的核心逻辑
|
||||||
|
func (g *GatewayOrchestrator) ProcessAPIRequest(ctx context.Context, req *APIRequest) (*APIResponse, error) {
|
||||||
|
// 1. 解析路由 - 确定目标域
|
||||||
|
route, err := g.routerService.ResolveRoute(req.Path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 根据不同的业务场景,实现不同的协调逻辑
|
||||||
|
switch route.Domain {
|
||||||
|
case "data":
|
||||||
|
return g.handleDataDomainRequest(ctx, req)
|
||||||
|
case "user":
|
||||||
|
return g.handleUserDomainRequest(ctx, req)
|
||||||
|
case "billing":
|
||||||
|
return g.handleBillingDomainRequest(ctx, req)
|
||||||
|
default:
|
||||||
|
return nil, errors.New("unknown domain")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🔥 数据域请求的协调逻辑 (你之前问的API调用流程)
|
||||||
|
func (g *GatewayOrchestrator) handleDataDomainRequest(ctx context.Context, req *APIRequest) (*APIResponse, error) {
|
||||||
|
// 注意:这里只做最基础的协调,复杂的业务逻辑在Data Domain内部
|
||||||
|
|
||||||
|
// 1. 前置验证 (网关层职责)
|
||||||
|
if err := g.validateBasicRequest(req); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 认证检查 (网关层职责)
|
||||||
|
userInfo, err := g.authenticateUser(ctx, req.Headers.Authorization)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 调用数据域服务 (核心业务逻辑在数据域内部)
|
||||||
|
dataReq := &data.ProcessAPIRequestRequest{
|
||||||
|
UserId: userInfo.UserId,
|
||||||
|
EnterpriseId: userInfo.EnterpriseId,
|
||||||
|
ApiPath: req.Path,
|
||||||
|
Parameters: req.Parameters,
|
||||||
|
ClientIp: req.ClientIP,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🚨 重点:复杂的跨域协调逻辑在数据域的DataOrchestrator中实现
|
||||||
|
dataResp, err := g.dataClient.ProcessAPIRequest(ctx, dataReq)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 响应转换 (网关层职责)
|
||||||
|
return g.convertToHTTPResponse(dataResp), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 简单的认证逻辑 (复杂认证在User Domain)
|
||||||
|
func (g *GatewayOrchestrator) authenticateUser(ctx context.Context, authHeader string) (*UserInfo, error) {
|
||||||
|
// 调用用户域进行认证
|
||||||
|
authReq := &user.AuthenticateRequest{
|
||||||
|
Token: authHeader,
|
||||||
|
}
|
||||||
|
|
||||||
|
authResp, err := g.userClient.Authenticate(ctx, authReq)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &UserInfo{
|
||||||
|
UserId: authResp.UserId,
|
||||||
|
EnterpriseId: authResp.EnterpriseId,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 数据域的协调逻辑 (Data Orchestrator)
|
||||||
|
|
||||||
|
```go
|
||||||
|
// domains/data-domain/data-service/internal/application/service/data_orchestrator.go
|
||||||
|
|
||||||
|
type DataOrchestrator struct {
|
||||||
|
// 数据域内部的业务服务
|
||||||
|
riskService *RiskAssessmentService // 原FLXG
|
||||||
|
creditService *CreditCheckService // 原JRZQ
|
||||||
|
companyService *CompanyInfoService // 原QYGL
|
||||||
|
queryService *DataQueryService // 原IVYZ
|
||||||
|
|
||||||
|
// 其他域的gRPC客户端
|
||||||
|
securityClient SecurityServiceClient // 安全域
|
||||||
|
userClient UserServiceClient // 用户域
|
||||||
|
productClient ProductServiceClient // 产品域
|
||||||
|
billingClient BillingServiceClient // 计费域
|
||||||
|
auditClient AuditServiceClient // 审计域
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🔥 这里实现你问的那个复杂API调用流程的核心协调逻辑
|
||||||
|
func (d *DataOrchestrator) ProcessAPIRequest(ctx context.Context, req *ProcessAPIRequestRequest) (*ProcessAPIResponseResponse, error) {
|
||||||
|
|
||||||
|
// === 第一阶段:安全验证 ===
|
||||||
|
|
||||||
|
// 1. IP白名单验证 (调用安全域)
|
||||||
|
whitelistReq := &security.CheckWhitelistRequest{
|
||||||
|
EnterpriseId: req.EnterpriseId,
|
||||||
|
ClientIp: req.ClientIp,
|
||||||
|
}
|
||||||
|
if _, err := d.securityClient.CheckWhitelist(ctx, whitelistReq); err != nil {
|
||||||
|
return nil, fmt.Errorf("IP白名单验证失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 企业ID验证 (调用用户域)
|
||||||
|
enterpriseReq := &user.ValidateEnterpriseRequest{
|
||||||
|
UserId: req.UserId,
|
||||||
|
EnterpriseId: req.EnterpriseId,
|
||||||
|
}
|
||||||
|
enterpriseResp, err := d.userClient.ValidateEnterprise(ctx, enterpriseReq)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("企业验证失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 密钥解密 (调用安全域)
|
||||||
|
decryptReq := &security.DecryptSecretRequest{
|
||||||
|
EnterpriseId: req.EnterpriseId,
|
||||||
|
EncryptedKey: enterpriseResp.EncryptedSecretKey,
|
||||||
|
}
|
||||||
|
decryptResp, err := d.securityClient.DecryptSecret(ctx, decryptReq)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("密钥解密失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// === 第二阶段:权限与产品验证 ===
|
||||||
|
|
||||||
|
// 4. 产品权限检查 (调用产品域)
|
||||||
|
productReq := &product.CheckProductAccessRequest{
|
||||||
|
UserId: req.UserId,
|
||||||
|
ApiPath: req.ApiPath,
|
||||||
|
SecretKey: decryptResp.SecretKey,
|
||||||
|
}
|
||||||
|
productResp, err := d.productClient.CheckProductAccess(ctx, productReq)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("产品权限检查失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 余额检查 (调用计费域)
|
||||||
|
balanceReq := &billing.CheckBalanceRequest{
|
||||||
|
UserId: req.UserId,
|
||||||
|
ProductCode: productResp.ProductCode,
|
||||||
|
ApiPath: req.ApiPath,
|
||||||
|
}
|
||||||
|
balanceResp, err := d.billingClient.CheckBalance(ctx, balanceReq)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("余额检查失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// === 第三阶段:业务处理 ===
|
||||||
|
|
||||||
|
// 6. 根据API路径选择对应的业务服务 (数据域内部逻辑)
|
||||||
|
var businessResult *BusinessResult
|
||||||
|
switch {
|
||||||
|
case strings.Contains(req.ApiPath, "risk-assessment"):
|
||||||
|
businessResult, err = d.riskService.ProcessRequest(ctx, req.Parameters)
|
||||||
|
case strings.Contains(req.ApiPath, "credit-check"):
|
||||||
|
businessResult, err = d.creditService.ProcessRequest(ctx, req.Parameters)
|
||||||
|
case strings.Contains(req.ApiPath, "company-info"):
|
||||||
|
businessResult, err = d.companyService.ProcessRequest(ctx, req.Parameters)
|
||||||
|
case strings.Contains(req.ApiPath, "data-query"):
|
||||||
|
businessResult, err = d.queryService.ProcessRequest(ctx, req.Parameters)
|
||||||
|
default:
|
||||||
|
return nil, errors.New("不支持的API路径")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("业务处理失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// === 第四阶段:计费与审计 ===
|
||||||
|
|
||||||
|
// 7. 计费扣款 (调用计费域)
|
||||||
|
chargeReq := &billing.ChargeRequest{
|
||||||
|
UserId: req.UserId,
|
||||||
|
ProductCode: productResp.ProductCode,
|
||||||
|
ApiPath: req.ApiPath,
|
||||||
|
Amount: balanceResp.RequiredAmount,
|
||||||
|
RequestId: generateRequestId(),
|
||||||
|
}
|
||||||
|
chargeResp, err := d.billingClient.Charge(ctx, chargeReq)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("计费失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 8. 记录审计日志 (调用审计域)
|
||||||
|
auditReq := &audit.RecordAPICallRequest{
|
||||||
|
UserId: req.UserId,
|
||||||
|
EnterpriseId: req.EnterpriseId,
|
||||||
|
ApiPath: req.ApiPath,
|
||||||
|
ClientIp: req.ClientIp,
|
||||||
|
RequestId: chargeResp.TransactionId,
|
||||||
|
Result: "success",
|
||||||
|
ResponseTime: time.Since(startTime).Milliseconds(),
|
||||||
|
}
|
||||||
|
go d.auditClient.RecordAPICall(context.Background(), auditReq) // 异步记录
|
||||||
|
|
||||||
|
// 9. 返回最终结果
|
||||||
|
return &ProcessAPIResponseResponse{
|
||||||
|
Success: true,
|
||||||
|
Data: businessResult.Data,
|
||||||
|
TransactionId: chargeResp.TransactionId,
|
||||||
|
RemainingBalance: chargeResp.RemainingBalance,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. HTTP 处理器 (接口适配器层)
|
||||||
|
|
||||||
|
```go
|
||||||
|
// domains/gateway-domain/gateway-service/internal/interfaces/http/data_handler.go
|
||||||
|
|
||||||
|
type DataHandler struct {
|
||||||
|
orchestrator *GatewayOrchestrator
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTTP入口点
|
||||||
|
func (h *DataHandler) HandleDataAPI(c *gin.Context) {
|
||||||
|
// 1. HTTP请求转换
|
||||||
|
req := &APIRequest{
|
||||||
|
Path: c.Request.URL.Path,
|
||||||
|
Method: c.Request.Method,
|
||||||
|
Parameters: extractParameters(c),
|
||||||
|
Headers: extractHeaders(c),
|
||||||
|
ClientIP: c.ClientIP(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 调用网关协调器
|
||||||
|
resp, err := h.orchestrator.ProcessAPIRequest(c.Request.Context(), req)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. HTTP响应
|
||||||
|
c.JSON(http.StatusOK, resp)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🌊 完整的调用流程
|
||||||
|
|
||||||
|
```
|
||||||
|
1. 客户端 HTTP请求
|
||||||
|
↓
|
||||||
|
2. 网关域 (Gateway Domain)
|
||||||
|
├── HTTP Handler (接口适配器层)
|
||||||
|
├── Gateway Orchestrator (应用层)
|
||||||
|
│ ├── 基础验证
|
||||||
|
│ ├── 用户认证 → User Domain
|
||||||
|
│ └── 路由到目标域
|
||||||
|
└── 调用 Data Domain
|
||||||
|
↓
|
||||||
|
3. 数据域 (Data Domain)
|
||||||
|
└── Data Orchestrator (应用层) 🔥 核心协调逻辑
|
||||||
|
├── 安全验证 → Security Domain
|
||||||
|
├── 企业验证 → User Domain
|
||||||
|
├── 权限检查 → Product Domain
|
||||||
|
├── 余额检查 → Billing Domain
|
||||||
|
├── 业务处理 (内部服务)
|
||||||
|
├── 计费扣款 → Billing Domain
|
||||||
|
└── 审计记录 → Audit Domain
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎯 总结:Logic 实现的分层
|
||||||
|
|
||||||
|
### 1. **网关域的职责** (简单协调)
|
||||||
|
|
||||||
|
- HTTP 协议处理
|
||||||
|
- 路由分发
|
||||||
|
- 基础认证
|
||||||
|
- 中间件处理 (限流、CORS 等)
|
||||||
|
|
||||||
|
### 2. **数据域的职责** (复杂协调) 🔥
|
||||||
|
|
||||||
|
- **这里是你问的那个复杂 API 调用流程的主要实现位置**
|
||||||
|
- 跨域服务协调
|
||||||
|
- 业务流程编排
|
||||||
|
- 事务一致性保证
|
||||||
|
|
||||||
|
### 3. **其他域的职责** (专业服务)
|
||||||
|
|
||||||
|
- 各自领域的专业逻辑
|
||||||
|
- 对外提供 gRPC 接口
|
||||||
|
|
||||||
|
**关键点:网关域负责"谁来处理",数据域负责"怎么处理"!**
|
||||||
766
网关管理详解.md
Normal file
766
网关管理详解.md
Normal file
@@ -0,0 +1,766 @@
|
|||||||
|
# 微服务网关管理详解
|
||||||
|
|
||||||
|
## 🚪 什么是 API 网关?
|
||||||
|
|
||||||
|
API 网关就像一个**智能门卫**,站在所有微服务的前面:
|
||||||
|
|
||||||
|
```
|
||||||
|
客户端请求 → API网关 → 路由到具体的微服务
|
||||||
|
↑ ↑ ↑
|
||||||
|
用户App 统一入口 用户服务
|
||||||
|
管理后台 ↓ 支付服务
|
||||||
|
第三方系统 认证授权 产品服务
|
||||||
|
限流控制 数据服务
|
||||||
|
日志记录
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎯 网关的核心功能
|
||||||
|
|
||||||
|
### 1. 请求路由 (Request Routing)
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 路由配置示例
|
||||||
|
type Route struct {
|
||||||
|
Path string `yaml:"path"`
|
||||||
|
Method string `yaml:"method"`
|
||||||
|
Service string `yaml:"service"`
|
||||||
|
Middlewares []string `yaml:"middlewares"`
|
||||||
|
Headers map[string]string `yaml:"headers"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var routes = []Route{
|
||||||
|
{
|
||||||
|
Path: "/api/v1/users/*",
|
||||||
|
Method: "*",
|
||||||
|
Service: "user-service",
|
||||||
|
Middlewares: []string{"auth", "rate-limit"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Path: "/api/v1/payments/*",
|
||||||
|
Method: "*",
|
||||||
|
Service: "payment-service",
|
||||||
|
Middlewares: []string{"auth", "encrypt"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Path: "/api/v1/products/*",
|
||||||
|
Method: "*",
|
||||||
|
Service: "product-service",
|
||||||
|
Middlewares: []string{"auth", "cache"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 负载均衡 (Load Balancing)
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 负载均衡器
|
||||||
|
type LoadBalancer interface {
|
||||||
|
Next() *ServiceInstance
|
||||||
|
}
|
||||||
|
|
||||||
|
// 轮询负载均衡
|
||||||
|
type RoundRobinBalancer struct {
|
||||||
|
instances []*ServiceInstance
|
||||||
|
current int
|
||||||
|
mutex sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rb *RoundRobinBalancer) Next() *ServiceInstance {
|
||||||
|
rb.mutex.Lock()
|
||||||
|
defer rb.mutex.Unlock()
|
||||||
|
|
||||||
|
if len(rb.instances) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
instance := rb.instances[rb.current]
|
||||||
|
rb.current = (rb.current + 1) % len(rb.instances)
|
||||||
|
return instance
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加权轮询
|
||||||
|
type WeightedRoundRobinBalancer struct {
|
||||||
|
instances []*WeightedInstance
|
||||||
|
total int
|
||||||
|
current int
|
||||||
|
}
|
||||||
|
|
||||||
|
type WeightedInstance struct {
|
||||||
|
Instance *ServiceInstance
|
||||||
|
Weight int
|
||||||
|
Current int
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 服务发现 (Service Discovery)
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 服务发现接口
|
||||||
|
type ServiceDiscovery interface {
|
||||||
|
Register(service *ServiceInstance) error
|
||||||
|
Deregister(serviceID string) error
|
||||||
|
Discover(serviceName string) ([]*ServiceInstance, error)
|
||||||
|
Watch(serviceName string) <-chan []*ServiceInstance
|
||||||
|
}
|
||||||
|
|
||||||
|
// Consul 服务发现实现
|
||||||
|
type ConsulDiscovery struct {
|
||||||
|
client *consul.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cd *ConsulDiscovery) Discover(serviceName string) ([]*ServiceInstance, error) {
|
||||||
|
services, _, err := cd.client.Health().Service(serviceName, "", true, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var instances []*ServiceInstance
|
||||||
|
for _, service := range services {
|
||||||
|
instances = append(instances, &ServiceInstance{
|
||||||
|
ID: service.Service.ID,
|
||||||
|
Name: service.Service.Service,
|
||||||
|
Address: service.Service.Address,
|
||||||
|
Port: service.Service.Port,
|
||||||
|
Meta: service.Service.Meta,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return instances, nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🛡️ 网关中间件系统
|
||||||
|
|
||||||
|
### 1. 认证中间件
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 认证中间件
|
||||||
|
func AuthMiddleware(jwtSecret string) gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
// 1. 获取 Token
|
||||||
|
token := extractToken(c)
|
||||||
|
if token == "" {
|
||||||
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "缺少认证令牌"})
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 验证 Token
|
||||||
|
claims, err := validateJWT(token, jwtSecret)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "无效的认证令牌"})
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 设置用户上下文
|
||||||
|
c.Set("user_id", claims.UserID)
|
||||||
|
c.Set("username", claims.Username)
|
||||||
|
c.Set("roles", claims.Roles)
|
||||||
|
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractToken(c *gin.Context) string {
|
||||||
|
// 从 Header 获取
|
||||||
|
authHeader := c.GetHeader("Authorization")
|
||||||
|
if authHeader != "" && strings.HasPrefix(authHeader, "Bearer ") {
|
||||||
|
return strings.TrimPrefix(authHeader, "Bearer ")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从 Query 参数获取
|
||||||
|
return c.Query("token")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 限流中间件
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 限流中间件
|
||||||
|
type RateLimiter struct {
|
||||||
|
redis *redis.Client
|
||||||
|
window time.Duration
|
||||||
|
limit int
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRateLimiter(redis *redis.Client, window time.Duration, limit int) *RateLimiter {
|
||||||
|
return &RateLimiter{
|
||||||
|
redis: redis,
|
||||||
|
window: window,
|
||||||
|
limit: limit,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rl *RateLimiter) Middleware() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
// 1. 获取客户端标识
|
||||||
|
clientID := getClientID(c)
|
||||||
|
|
||||||
|
// 2. 检查限流
|
||||||
|
allowed, err := rl.isAllowed(clientID)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "限流检查失败"})
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !allowed {
|
||||||
|
c.JSON(http.StatusTooManyRequests, gin.H{
|
||||||
|
"error": "请求过于频繁,请稍后再试",
|
||||||
|
"retry_after": int(rl.window.Seconds()),
|
||||||
|
})
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rl *RateLimiter) isAllowed(clientID string) (bool, error) {
|
||||||
|
key := fmt.Sprintf("rate_limit:%s", clientID)
|
||||||
|
|
||||||
|
// 使用滑动窗口算法
|
||||||
|
now := time.Now().Unix()
|
||||||
|
window := now - int64(rl.window.Seconds())
|
||||||
|
|
||||||
|
pipe := rl.redis.Pipeline()
|
||||||
|
|
||||||
|
// 删除过期的记录
|
||||||
|
pipe.ZRemRangeByScore(context.Background(), key, "0", fmt.Sprintf("%d", window))
|
||||||
|
|
||||||
|
// 获取当前窗口内的请求数
|
||||||
|
countCmd := pipe.ZCard(context.Background(), key)
|
||||||
|
|
||||||
|
// 添加当前请求
|
||||||
|
pipe.ZAdd(context.Background(), key, &redis.Z{
|
||||||
|
Score: float64(now),
|
||||||
|
Member: fmt.Sprintf("%d", now),
|
||||||
|
})
|
||||||
|
|
||||||
|
// 设置过期时间
|
||||||
|
pipe.Expire(context.Background(), key, rl.window)
|
||||||
|
|
||||||
|
_, err := pipe.Exec(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
count := countCmd.Val()
|
||||||
|
return count < int64(rl.limit), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getClientID(c *gin.Context) string {
|
||||||
|
// 优先使用用户ID
|
||||||
|
if userID := c.GetString("user_id"); userID != "" {
|
||||||
|
return "user:" + userID
|
||||||
|
}
|
||||||
|
|
||||||
|
// 其次使用API Key
|
||||||
|
if apiKey := c.GetHeader("X-API-Key"); apiKey != "" {
|
||||||
|
return "api:" + apiKey
|
||||||
|
}
|
||||||
|
|
||||||
|
// 最后使用IP地址
|
||||||
|
return "ip:" + c.ClientIP()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 缓存中间件
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 缓存中间件
|
||||||
|
type CacheMiddleware struct {
|
||||||
|
redis *redis.Client
|
||||||
|
ttl time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCacheMiddleware(redis *redis.Client, ttl time.Duration) *CacheMiddleware {
|
||||||
|
return &CacheMiddleware{
|
||||||
|
redis: redis,
|
||||||
|
ttl: ttl,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cm *CacheMiddleware) Middleware() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
// 只缓存 GET 请求
|
||||||
|
if c.Request.Method != http.MethodGet {
|
||||||
|
c.Next()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成缓存键
|
||||||
|
cacheKey := generateCacheKey(c)
|
||||||
|
|
||||||
|
// 尝试从缓存获取
|
||||||
|
cached, err := cm.redis.Get(context.Background(), cacheKey).Result()
|
||||||
|
if err == nil {
|
||||||
|
var response CachedResponse
|
||||||
|
if json.Unmarshal([]byte(cached), &response) == nil {
|
||||||
|
// 设置响应头
|
||||||
|
for key, value := range response.Headers {
|
||||||
|
c.Header(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Data(response.Status, response.ContentType, response.Body)
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 包装 ResponseWriter 来捕获响应
|
||||||
|
crw := &cachedResponseWriter{
|
||||||
|
ResponseWriter: c.Writer,
|
||||||
|
body: &bytes.Buffer{},
|
||||||
|
headers: make(map[string]string),
|
||||||
|
}
|
||||||
|
c.Writer = crw
|
||||||
|
|
||||||
|
c.Next()
|
||||||
|
|
||||||
|
// 缓存响应
|
||||||
|
if crw.status >= 200 && crw.status < 300 {
|
||||||
|
response := CachedResponse{
|
||||||
|
Status: crw.status,
|
||||||
|
Headers: crw.headers,
|
||||||
|
ContentType: crw.Header().Get("Content-Type"),
|
||||||
|
Body: crw.body.Bytes(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if data, err := json.Marshal(response); err == nil {
|
||||||
|
cm.redis.Set(context.Background(), cacheKey, data, cm.ttl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type CachedResponse struct {
|
||||||
|
Status int `json:"status"`
|
||||||
|
Headers map[string]string `json:"headers"`
|
||||||
|
ContentType string `json:"content_type"`
|
||||||
|
Body []byte `json:"body"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type cachedResponseWriter struct {
|
||||||
|
gin.ResponseWriter
|
||||||
|
body *bytes.Buffer
|
||||||
|
headers map[string]string
|
||||||
|
status int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (crw *cachedResponseWriter) Write(data []byte) (int, error) {
|
||||||
|
crw.body.Write(data)
|
||||||
|
return crw.ResponseWriter.Write(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (crw *cachedResponseWriter) WriteHeader(status int) {
|
||||||
|
crw.status = status
|
||||||
|
crw.ResponseWriter.WriteHeader(status)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 网关配置管理
|
||||||
|
|
||||||
|
### 1. 动态配置
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# gateway.yaml
|
||||||
|
gateway:
|
||||||
|
port: 8080
|
||||||
|
timeout: 30s
|
||||||
|
|
||||||
|
services:
|
||||||
|
user-service:
|
||||||
|
discovery: consul
|
||||||
|
load_balancer: round_robin
|
||||||
|
health_check:
|
||||||
|
enabled: true
|
||||||
|
interval: 10s
|
||||||
|
timeout: 3s
|
||||||
|
path: /health
|
||||||
|
circuit_breaker:
|
||||||
|
enabled: true
|
||||||
|
failure_threshold: 5
|
||||||
|
timeout: 60s
|
||||||
|
|
||||||
|
payment-service:
|
||||||
|
discovery: consul
|
||||||
|
load_balancer: weighted_round_robin
|
||||||
|
weights:
|
||||||
|
- instance: payment-service-1
|
||||||
|
weight: 3
|
||||||
|
- instance: payment-service-2
|
||||||
|
weight: 1
|
||||||
|
|
||||||
|
middlewares:
|
||||||
|
auth:
|
||||||
|
jwt_secret: ${JWT_SECRET}
|
||||||
|
excluded_paths:
|
||||||
|
- /api/v1/auth/login
|
||||||
|
- /api/v1/auth/register
|
||||||
|
- /health
|
||||||
|
|
||||||
|
rate_limit:
|
||||||
|
default:
|
||||||
|
window: 60s
|
||||||
|
limit: 100
|
||||||
|
user_limits:
|
||||||
|
premium_user:
|
||||||
|
window: 60s
|
||||||
|
limit: 1000
|
||||||
|
basic_user:
|
||||||
|
window: 60s
|
||||||
|
limit: 100
|
||||||
|
|
||||||
|
cache:
|
||||||
|
ttl: 300s
|
||||||
|
excluded_paths:
|
||||||
|
- /api/v1/users/me
|
||||||
|
- /api/v1/payments/*
|
||||||
|
|
||||||
|
routes:
|
||||||
|
- path: /api/v1/users/*
|
||||||
|
service: user-service
|
||||||
|
middlewares: [auth, rate_limit]
|
||||||
|
|
||||||
|
- path: /api/v1/payments/*
|
||||||
|
service: payment-service
|
||||||
|
middlewares: [auth, rate_limit]
|
||||||
|
|
||||||
|
- path: /api/v1/products/*
|
||||||
|
service: product-service
|
||||||
|
middlewares: [auth, rate_limit, cache]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 配置热更新
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 配置管理器
|
||||||
|
type ConfigManager struct {
|
||||||
|
config *GatewayConfig
|
||||||
|
mutex sync.RWMutex
|
||||||
|
watchers []chan *GatewayConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewConfigManager() *ConfigManager {
|
||||||
|
return &ConfigManager{
|
||||||
|
watchers: make([]chan *GatewayConfig, 0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cm *ConfigManager) LoadConfig(path string) error {
|
||||||
|
cm.mutex.Lock()
|
||||||
|
defer cm.mutex.Unlock()
|
||||||
|
|
||||||
|
data, err := ioutil.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var config GatewayConfig
|
||||||
|
if err := yaml.Unmarshal(data, &config); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
cm.config = &config
|
||||||
|
|
||||||
|
// 通知所有监听者
|
||||||
|
for _, watcher := range cm.watchers {
|
||||||
|
select {
|
||||||
|
case watcher <- &config:
|
||||||
|
default:
|
||||||
|
// 非阻塞发送
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cm *ConfigManager) Watch() <-chan *GatewayConfig {
|
||||||
|
cm.mutex.Lock()
|
||||||
|
defer cm.mutex.Unlock()
|
||||||
|
|
||||||
|
watcher := make(chan *GatewayConfig, 1)
|
||||||
|
cm.watchers = append(cm.watchers, watcher)
|
||||||
|
|
||||||
|
// 立即发送当前配置
|
||||||
|
if cm.config != nil {
|
||||||
|
watcher <- cm.config
|
||||||
|
}
|
||||||
|
|
||||||
|
return watcher
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cm *ConfigManager) WatchFile(path string) {
|
||||||
|
watcher, err := fsnotify.NewWatcher()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
defer watcher.Close()
|
||||||
|
|
||||||
|
err = watcher.Add(path)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case event := <-watcher.Events:
|
||||||
|
if event.Op&fsnotify.Write == fsnotify.Write {
|
||||||
|
log.Println("配置文件已修改,重新加载...")
|
||||||
|
time.Sleep(100 * time.Millisecond) // 避免多次触发
|
||||||
|
cm.LoadConfig(path)
|
||||||
|
}
|
||||||
|
case err := <-watcher.Errors:
|
||||||
|
log.Println("配置文件监听错误:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📊 网关监控和可观测性
|
||||||
|
|
||||||
|
### 1. 指标收集
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 指标收集
|
||||||
|
var (
|
||||||
|
requestsTotal = prometheus.NewCounterVec(
|
||||||
|
prometheus.CounterOpts{
|
||||||
|
Name: "gateway_requests_total",
|
||||||
|
Help: "Total number of requests",
|
||||||
|
},
|
||||||
|
[]string{"service", "method", "path", "status"},
|
||||||
|
)
|
||||||
|
|
||||||
|
requestDuration = prometheus.NewHistogramVec(
|
||||||
|
prometheus.HistogramOpts{
|
||||||
|
Name: "gateway_request_duration_seconds",
|
||||||
|
Help: "Request duration in seconds",
|
||||||
|
Buckets: prometheus.DefBuckets,
|
||||||
|
},
|
||||||
|
[]string{"service", "method", "path"},
|
||||||
|
)
|
||||||
|
|
||||||
|
activeConnections = prometheus.NewGauge(
|
||||||
|
prometheus.GaugeOpts{
|
||||||
|
Name: "gateway_active_connections",
|
||||||
|
Help: "Number of active connections",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
func MetricsMiddleware() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
start := time.Now()
|
||||||
|
|
||||||
|
activeConnections.Inc()
|
||||||
|
defer activeConnections.Dec()
|
||||||
|
|
||||||
|
c.Next()
|
||||||
|
|
||||||
|
duration := time.Since(start).Seconds()
|
||||||
|
status := strconv.Itoa(c.Writer.Status())
|
||||||
|
service := c.GetString("target_service")
|
||||||
|
|
||||||
|
requestsTotal.WithLabelValues(
|
||||||
|
service,
|
||||||
|
c.Request.Method,
|
||||||
|
c.Request.URL.Path,
|
||||||
|
status,
|
||||||
|
).Inc()
|
||||||
|
|
||||||
|
requestDuration.WithLabelValues(
|
||||||
|
service,
|
||||||
|
c.Request.Method,
|
||||||
|
c.Request.URL.Path,
|
||||||
|
).Observe(duration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 链路追踪
|
||||||
|
|
||||||
|
```go
|
||||||
|
// OpenTelemetry 追踪
|
||||||
|
func TracingMiddleware(tracer trace.Tracer) gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
spanName := fmt.Sprintf("%s %s", c.Request.Method, c.Request.URL.Path)
|
||||||
|
|
||||||
|
ctx, span := tracer.Start(c.Request.Context(), spanName)
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
|
// 设置 span 属性
|
||||||
|
span.SetAttributes(
|
||||||
|
attribute.String("http.method", c.Request.Method),
|
||||||
|
attribute.String("http.url", c.Request.URL.String()),
|
||||||
|
attribute.String("http.user_agent", c.Request.UserAgent()),
|
||||||
|
attribute.String("client.ip", c.ClientIP()),
|
||||||
|
)
|
||||||
|
|
||||||
|
// 传递上下文
|
||||||
|
c.Request = c.Request.WithContext(ctx)
|
||||||
|
|
||||||
|
c.Next()
|
||||||
|
|
||||||
|
// 设置响应属性
|
||||||
|
span.SetAttributes(
|
||||||
|
attribute.Int("http.status_code", c.Writer.Status()),
|
||||||
|
attribute.Int("http.response_size", c.Writer.Size()),
|
||||||
|
)
|
||||||
|
|
||||||
|
if c.Writer.Status() >= 400 {
|
||||||
|
span.SetStatus(codes.Error, fmt.Sprintf("HTTP %d", c.Writer.Status()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🏗️ 网关架构设计
|
||||||
|
|
||||||
|
### 1. 高可用部署
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# docker-compose.yml
|
||||||
|
version: "3.8"
|
||||||
|
services:
|
||||||
|
gateway-1:
|
||||||
|
image: your-gateway:latest
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
|
environment:
|
||||||
|
- GATEWAY_ID=gateway-1
|
||||||
|
- CONSUL_ADDRESS=consul:8500
|
||||||
|
depends_on:
|
||||||
|
- consul
|
||||||
|
- redis
|
||||||
|
|
||||||
|
gateway-2:
|
||||||
|
image: your-gateway:latest
|
||||||
|
ports:
|
||||||
|
- "8081:8080"
|
||||||
|
environment:
|
||||||
|
- GATEWAY_ID=gateway-2
|
||||||
|
- CONSUL_ADDRESS=consul:8500
|
||||||
|
depends_on:
|
||||||
|
- consul
|
||||||
|
- redis
|
||||||
|
|
||||||
|
nginx:
|
||||||
|
image: nginx:alpine
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
volumes:
|
||||||
|
- ./nginx.conf:/etc/nginx/nginx.conf
|
||||||
|
depends_on:
|
||||||
|
- gateway-1
|
||||||
|
- gateway-2
|
||||||
|
|
||||||
|
consul:
|
||||||
|
image: consul:latest
|
||||||
|
ports:
|
||||||
|
- "8500:8500"
|
||||||
|
command: consul agent -dev -client=0.0.0.0
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis:alpine
|
||||||
|
ports:
|
||||||
|
- "6379:6379"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Nginx 负载均衡配置
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
# nginx.conf
|
||||||
|
upstream gateway {
|
||||||
|
server gateway-1:8080 weight=1 max_fails=3 fail_timeout=30s;
|
||||||
|
server gateway-2:8080 weight=1 max_fails=3 fail_timeout=30s;
|
||||||
|
keepalive 32;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name api.example.com;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://gateway;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection 'upgrade';
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_cache_bypass $http_upgrade;
|
||||||
|
|
||||||
|
# 健康检查
|
||||||
|
proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504;
|
||||||
|
proxy_connect_timeout 5s;
|
||||||
|
proxy_send_timeout 60s;
|
||||||
|
proxy_read_timeout 60s;
|
||||||
|
}
|
||||||
|
|
||||||
|
# 健康检查端点
|
||||||
|
location /health {
|
||||||
|
access_log off;
|
||||||
|
proxy_pass http://gateway;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 实施建议
|
||||||
|
|
||||||
|
### 1. 渐进式迁移
|
||||||
|
|
||||||
|
```
|
||||||
|
阶段1: 搭建基础网关
|
||||||
|
├── 基本的路由功能
|
||||||
|
├── 健康检查
|
||||||
|
└── 监控指标
|
||||||
|
|
||||||
|
阶段2: 添加安全功能
|
||||||
|
├── JWT 认证
|
||||||
|
├── API 限流
|
||||||
|
└── 请求日志
|
||||||
|
|
||||||
|
阶段3: 性能优化
|
||||||
|
├── 响应缓存
|
||||||
|
├── 连接池
|
||||||
|
└── 负载均衡
|
||||||
|
|
||||||
|
阶段4: 高级功能
|
||||||
|
├── 熔断器
|
||||||
|
├── 灰度发布
|
||||||
|
└── API 版本管理
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 网关选型建议
|
||||||
|
|
||||||
|
**自研网关 (推荐给你)**:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 基于 gin + consul + redis 的轻量级网关
|
||||||
|
// 优势: 完全控制、定制化强、集成容易
|
||||||
|
// 适合: 中小型项目、快速迭代
|
||||||
|
```
|
||||||
|
|
||||||
|
**开源网关**:
|
||||||
|
|
||||||
|
- **Kong**: 功能丰富,插件生态好
|
||||||
|
- **Traefik**: 配置简单,支持多种后端
|
||||||
|
- **Envoy**: 性能极高,配置复杂
|
||||||
|
|
||||||
|
## 📋 总结
|
||||||
|
|
||||||
|
网关管理的核心要点:
|
||||||
|
|
||||||
|
1. **统一入口** - 所有外部请求通过网关
|
||||||
|
2. **服务路由** - 将请求路由到正确的后端服务
|
||||||
|
3. **安全认证** - 统一的身份验证和授权
|
||||||
|
4. **流量控制** - 限流、熔断、负载均衡
|
||||||
|
5. **可观测性** - 监控、日志、链路追踪
|
||||||
|
6. **高可用** - 多实例部署、健康检查
|
||||||
|
|
||||||
|
对于你的项目,建议先实现基础的路由和认证功能,然后逐步添加高级特性。
|
||||||
610
重构方案.md
Normal file
610
重构方案.md
Normal file
@@ -0,0 +1,610 @@
|
|||||||
|
# 天远 API 服务器重构方案
|
||||||
|
|
||||||
|
## 🎯 重构目标
|
||||||
|
|
||||||
|
1. **提高代码可维护性** - 清晰的业务边界和模块划分
|
||||||
|
2. **增强系统扩展性** - 松耦合的微服务架构
|
||||||
|
3. **改善开发效率** - 标准化的开发流程和工具链
|
||||||
|
4. **提升系统可靠性** - 完善的监控、测试和部署体系
|
||||||
|
|
||||||
|
## 🏗️ 架构重构
|
||||||
|
|
||||||
|
### 1. 业务域重新设计 (DDD)
|
||||||
|
|
||||||
|
```
|
||||||
|
新的业务域架构:
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ API Gateway │
|
||||||
|
│ (统一入口网关) │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
┌──────────────┼──────────────┐
|
||||||
|
│ │ │
|
||||||
|
┌───────────────▼──┐ ┌─────────▼──────┐ ┌────▼──────────┐
|
||||||
|
│ 用户域服务 │ │ 金融域服务 │ │ 数据服务域 │
|
||||||
|
│ (User Domain) │ │(Financial Domain)│ │(Data Domain) │
|
||||||
|
├─────────────────┤ ├─────────────────┤ ├──────────────┤
|
||||||
|
│• 用户认证 │ │• 钱包管理 │ │• 风险评估 │
|
||||||
|
│• 用户信息 │ │• 支付处理 │ │• 征信查询 │
|
||||||
|
│• 企业认证 │ │• 计费结算 │ │• 企业信息 │
|
||||||
|
│• 权限管理 │ │• 财务报表 │ │• 数据查询 │
|
||||||
|
└─────────────────┘ └─────────────────┘ └──────────────┘
|
||||||
|
│
|
||||||
|
┌──────────────┼──────────────┐
|
||||||
|
│ │ │
|
||||||
|
┌───────────────▼──┐ ┌─────────▼──────┐ ┌────▼──────────┐
|
||||||
|
│ 产品域服务 │ │ 平台域服务 │ │ 基础设施域 │
|
||||||
|
│(Product Domain) │ │(Platform Domain)│ │(Infra Domain) │
|
||||||
|
├─────────────────┤ ├─────────────────┤ ├──────────────┤
|
||||||
|
│• 产品目录 │ │• 管理后台 │ │• 消息队列 │
|
||||||
|
│• 产品订阅 │ │• 系统监控 │ │• 缓存服务 │
|
||||||
|
│• 访问控制 │ │• 日志管理 │ │• 配置中心 │
|
||||||
|
│• 白名单管理 │ │• 运维工具 │ │• 服务发现 │
|
||||||
|
└─────────────────┘ └─────────────────┘ └──────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 微服务重新设计
|
||||||
|
|
||||||
|
#### 用户域 (User Domain)
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 用户认证服务
|
||||||
|
user-auth-service/
|
||||||
|
├── api/
|
||||||
|
│ ├── auth.proto # gRPC 接口定义
|
||||||
|
│ └── auth.api # HTTP 接口定义
|
||||||
|
├── internal/
|
||||||
|
│ ├── domain/ # 领域模型
|
||||||
|
│ ├── application/ # 应用服务
|
||||||
|
│ ├── infrastructure/ # 基础设施
|
||||||
|
│ └── interfaces/ # 接口适配器
|
||||||
|
└── cmd/
|
||||||
|
└── main.go
|
||||||
|
|
||||||
|
// 企业认证服务
|
||||||
|
enterprise-service/
|
||||||
|
├── api/
|
||||||
|
├── internal/
|
||||||
|
└── cmd/
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 金融域 (Financial Domain)
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 钱包服务
|
||||||
|
wallet-service/
|
||||||
|
├── api/
|
||||||
|
├── internal/
|
||||||
|
│ ├── domain/
|
||||||
|
│ │ ├── wallet/ # 钱包聚合根
|
||||||
|
│ │ ├── transaction/ # 交易实体
|
||||||
|
│ │ └── balance/ # 余额值对象
|
||||||
|
│ ├── application/
|
||||||
|
│ └── infrastructure/
|
||||||
|
└── cmd/
|
||||||
|
|
||||||
|
// 支付服务
|
||||||
|
payment-service/
|
||||||
|
├── api/
|
||||||
|
├── internal/
|
||||||
|
└── cmd/
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 数据服务域 (Data Service Domain)
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 统一数据服务网关
|
||||||
|
data-gateway-service/
|
||||||
|
├── api/
|
||||||
|
├── internal/
|
||||||
|
│ ├── adapters/ # 第三方数据源适配器
|
||||||
|
│ │ ├── westdex/ # 西部数据源
|
||||||
|
│ │ ├── baidu/ # 百度接口
|
||||||
|
│ │ └── aliyun/ # 阿里云接口
|
||||||
|
│ ├── domain/
|
||||||
|
│ │ ├── query/ # 查询服务
|
||||||
|
│ │ ├── risk/ # 风险评估
|
||||||
|
│ │ └── credit/ # 征信服务
|
||||||
|
│ └── application/
|
||||||
|
└── cmd/
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 产品域 (Product Domain)
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 产品管理服务
|
||||||
|
product-service/
|
||||||
|
├── api/
|
||||||
|
├── internal/
|
||||||
|
│ ├── domain/
|
||||||
|
│ │ ├── product/ # 产品聚合根
|
||||||
|
│ │ ├── subscription/ # 订阅实体
|
||||||
|
│ │ └── access/ # 访问控制
|
||||||
|
│ └── application/
|
||||||
|
└── cmd/
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🛠️ 技术重构
|
||||||
|
|
||||||
|
### 1. 代码结构标准化
|
||||||
|
|
||||||
|
**采用六边形架构 (Hexagonal Architecture)**:
|
||||||
|
|
||||||
|
```go
|
||||||
|
service/
|
||||||
|
├── api/ # 接口定义
|
||||||
|
│ ├── grpc/ # gRPC 协议
|
||||||
|
│ ├── http/ # HTTP 协议
|
||||||
|
│ └── event/ # 事件协议
|
||||||
|
├── internal/
|
||||||
|
│ ├── domain/ # 领域层
|
||||||
|
│ │ ├── entity/ # 实体
|
||||||
|
│ │ ├── valueobject/ # 值对象
|
||||||
|
│ │ ├── aggregate/ # 聚合根
|
||||||
|
│ │ ├── repository/ # 仓储接口
|
||||||
|
│ │ └── service/ # 领域服务
|
||||||
|
│ ├── application/ # 应用层
|
||||||
|
│ │ ├── command/ # 命令处理
|
||||||
|
│ │ ├── query/ # 查询处理
|
||||||
|
│ │ ├── dto/ # 数据传输对象
|
||||||
|
│ │ └── service/ # 应用服务
|
||||||
|
│ ├── infrastructure/ # 基础设施层
|
||||||
|
│ │ ├── persistence/ # 持久化
|
||||||
|
│ │ ├── messaging/ # 消息传递
|
||||||
|
│ │ ├── external/ # 外部服务
|
||||||
|
│ │ └── config/ # 配置
|
||||||
|
│ └── interfaces/ # 接口适配器层
|
||||||
|
│ ├── grpc/ # gRPC 适配器
|
||||||
|
│ ├── http/ # HTTP 适配器
|
||||||
|
│ ├── event/ # 事件适配器
|
||||||
|
│ └── cli/ # 命令行适配器
|
||||||
|
├── pkg/ # 共享包
|
||||||
|
├── test/ # 测试
|
||||||
|
├── docs/ # 文档
|
||||||
|
├── deployments/ # 部署配置
|
||||||
|
├── Dockerfile
|
||||||
|
└── go.mod
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 技术栈升级
|
||||||
|
|
||||||
|
**推荐的技术栈**:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# 微服务框架
|
||||||
|
framework: go-zero v1.6+ 或 go-kit v0.12+
|
||||||
|
|
||||||
|
# 数据库
|
||||||
|
database:
|
||||||
|
- PostgreSQL 15+ (替代MySQL, 更好的JSON支持)
|
||||||
|
- Redis 7+ (缓存和会话)
|
||||||
|
- ClickHouse (日志和分析)
|
||||||
|
|
||||||
|
# 消息队列
|
||||||
|
messaging:
|
||||||
|
- Apache Kafka 3.0+ (事件流)
|
||||||
|
- RabbitMQ 3.11+ (任务队列)
|
||||||
|
|
||||||
|
# 服务治理
|
||||||
|
service_mesh: Istio 1.17+
|
||||||
|
service_discovery: Consul 1.15+
|
||||||
|
configuration: Consul KV 或 etcd
|
||||||
|
|
||||||
|
# 监控和可观测性
|
||||||
|
monitoring:
|
||||||
|
- Prometheus + Grafana
|
||||||
|
- Jaeger (分布式追踪)
|
||||||
|
- ELK Stack (日志)
|
||||||
|
- SkyWalking (APM)
|
||||||
|
|
||||||
|
# 安全
|
||||||
|
security:
|
||||||
|
- Vault (密钥管理)
|
||||||
|
- OPA (策略引擎)
|
||||||
|
- Keycloak (身份认证)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📊 数据层重构
|
||||||
|
|
||||||
|
### 1. 数据库重新设计
|
||||||
|
|
||||||
|
**按业务域分离数据库**:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 用户域数据库
|
||||||
|
user_db:
|
||||||
|
- users
|
||||||
|
- user_profiles
|
||||||
|
- user_sessions
|
||||||
|
- enterprise_auth
|
||||||
|
|
||||||
|
-- 金融域数据库
|
||||||
|
financial_db:
|
||||||
|
- wallets
|
||||||
|
- transactions
|
||||||
|
- payment_orders
|
||||||
|
- billing_records
|
||||||
|
|
||||||
|
-- 产品域数据库
|
||||||
|
product_db:
|
||||||
|
- products
|
||||||
|
- product_subscriptions
|
||||||
|
- access_policies
|
||||||
|
- whitelists
|
||||||
|
|
||||||
|
-- 平台域数据库
|
||||||
|
platform_db:
|
||||||
|
- api_logs
|
||||||
|
- system_configs
|
||||||
|
- admin_users
|
||||||
|
- audit_logs
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 数据一致性策略
|
||||||
|
|
||||||
|
**事件驱动架构**:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 事件定义
|
||||||
|
type UserRegistered struct {
|
||||||
|
UserID int64 `json:"user_id"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
Timestamp time.Time `json:"timestamp"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type WalletCreated struct {
|
||||||
|
UserID int64 `json:"user_id"`
|
||||||
|
WalletID string `json:"wallet_id"`
|
||||||
|
Balance float64 `json:"balance"`
|
||||||
|
Timestamp time.Time `json:"timestamp"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 事件处理
|
||||||
|
func (h *WalletEventHandler) HandleUserRegistered(event UserRegistered) error {
|
||||||
|
// 为新用户创建钱包
|
||||||
|
return h.walletService.CreateWallet(event.UserID)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 开发流程重构
|
||||||
|
|
||||||
|
### 1. 代码生成和模板
|
||||||
|
|
||||||
|
**统一的代码生成器**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 使用 protobuf 和 API 定义生成代码
|
||||||
|
make generate-service name=user-auth
|
||||||
|
make generate-api name=wallet
|
||||||
|
|
||||||
|
# 生成的目录结构
|
||||||
|
generated/
|
||||||
|
├── user-auth-service/
|
||||||
|
│ ├── pb/ # protobuf 生成
|
||||||
|
│ ├── client/ # 客户端代码
|
||||||
|
│ ├── server/ # 服务端代码
|
||||||
|
│ └── mock/ # 测试桩
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 测试策略
|
||||||
|
|
||||||
|
**多层次测试**:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 单元测试
|
||||||
|
func TestWalletService_Transfer(t *testing.T) {
|
||||||
|
// 领域逻辑测试
|
||||||
|
}
|
||||||
|
|
||||||
|
// 集成测试
|
||||||
|
func TestWalletAPI_Transfer(t *testing.T) {
|
||||||
|
// API 集成测试
|
||||||
|
}
|
||||||
|
|
||||||
|
// 契约测试
|
||||||
|
func TestWalletService_Contract(t *testing.T) {
|
||||||
|
// 服务间契约测试
|
||||||
|
}
|
||||||
|
|
||||||
|
// 端到端测试
|
||||||
|
func TestTransferWorkflow(t *testing.T) {
|
||||||
|
// 完整业务流程测试
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. CI/CD 流水线
|
||||||
|
|
||||||
|
**GitOps 工作流**:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# .github/workflows/service.yml
|
||||||
|
name: Service CI/CD
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
paths: ["services/user-auth/**"]
|
||||||
|
pull_request:
|
||||||
|
paths: ["services/user-auth/**"]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- name: Run tests
|
||||||
|
run: make test-user-auth
|
||||||
|
|
||||||
|
build:
|
||||||
|
needs: test
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Build image
|
||||||
|
run: make build-user-auth
|
||||||
|
- name: Push to registry
|
||||||
|
run: make push-user-auth
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
needs: build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: github.ref == 'refs/heads/main'
|
||||||
|
steps:
|
||||||
|
- name: Deploy to staging
|
||||||
|
run: make deploy-user-auth-staging
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📱 API 设计重构
|
||||||
|
|
||||||
|
### 1. RESTful API 标准化
|
||||||
|
|
||||||
|
**统一的 API 设计规范**:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 资源命名
|
||||||
|
GET /api/v1/users # 获取用户列表
|
||||||
|
GET /api/v1/users/{id} # 获取用户详情
|
||||||
|
POST /api/v1/users # 创建用户
|
||||||
|
PUT /api/v1/users/{id} # 更新用户
|
||||||
|
DELETE /api/v1/users/{id} # 删除用户
|
||||||
|
|
||||||
|
// 子资源
|
||||||
|
GET /api/v1/users/{id}/wallets # 获取用户钱包
|
||||||
|
POST /api/v1/users/{id}/wallets # 创建用户钱包
|
||||||
|
|
||||||
|
// 统一响应格式
|
||||||
|
type APIResponse struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Data interface{} `json:"data,omitempty"`
|
||||||
|
Meta *Meta `json:"meta,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Meta struct {
|
||||||
|
Page int `json:"page,omitempty"`
|
||||||
|
PerPage int `json:"per_page,omitempty"`
|
||||||
|
Total int `json:"total,omitempty"`
|
||||||
|
TotalPages int `json:"total_pages,omitempty"`
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. GraphQL 支持
|
||||||
|
|
||||||
|
**为复杂查询提供 GraphQL**:
|
||||||
|
|
||||||
|
```graphql
|
||||||
|
type User {
|
||||||
|
id: ID!
|
||||||
|
username: String!
|
||||||
|
email: String!
|
||||||
|
wallet: Wallet
|
||||||
|
subscriptions: [ProductSubscription!]!
|
||||||
|
}
|
||||||
|
|
||||||
|
type Query {
|
||||||
|
user(id: ID!): User
|
||||||
|
users(filter: UserFilter, pagination: Pagination): UserConnection
|
||||||
|
}
|
||||||
|
|
||||||
|
type Mutation {
|
||||||
|
createUser(input: CreateUserInput!): User!
|
||||||
|
updateUser(id: ID!, input: UpdateUserInput!): User!
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔒 安全重构
|
||||||
|
|
||||||
|
### 1. 认证授权统一化
|
||||||
|
|
||||||
|
**OAuth 2.0 + RBAC**:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 角色定义
|
||||||
|
type Role struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Permissions []Permission `json:"permissions"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Permission struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Resource string `json:"resource"` // users, wallets, products
|
||||||
|
Action string `json:"action"` // read, write, delete
|
||||||
|
Scope string `json:"scope"` // own, team, all
|
||||||
|
}
|
||||||
|
|
||||||
|
// 中间件
|
||||||
|
func AuthMiddleware() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
token := extractToken(c)
|
||||||
|
claims, err := validateToken(token)
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithStatus(http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := getUserFromClaims(claims)
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithStatus(http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Set("user", user)
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 数据安全
|
||||||
|
|
||||||
|
**端到端加密 + 数据脱敏**:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 字段级加密
|
||||||
|
type User struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
Email string `json:"email" encrypt:"true"`
|
||||||
|
Phone string `json:"phone" encrypt:"true" mask:"***-****-####"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自动加密解密
|
||||||
|
func (u *User) BeforeSave() error {
|
||||||
|
return encrypt.EncryptStruct(u)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *User) AfterFind() error {
|
||||||
|
return encrypt.DecryptStruct(u)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📈 监控和可观测性
|
||||||
|
|
||||||
|
### 1. 分布式追踪
|
||||||
|
|
||||||
|
**OpenTelemetry 集成**:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func (s *UserService) CreateUser(ctx context.Context, req *pb.CreateUserRequest) (*pb.User, error) {
|
||||||
|
span := trace.SpanFromContext(ctx)
|
||||||
|
span.SetAttributes(
|
||||||
|
attribute.String("user.username", req.Username),
|
||||||
|
attribute.String("operation", "create_user"),
|
||||||
|
)
|
||||||
|
|
||||||
|
// 业务逻辑
|
||||||
|
user, err := s.userRepo.Create(ctx, req)
|
||||||
|
if err != nil {
|
||||||
|
span.RecordError(err)
|
||||||
|
span.SetStatus(codes.Error, err.Error())
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送事件
|
||||||
|
event := UserCreatedEvent{UserID: user.ID, Username: user.Username}
|
||||||
|
if err := s.eventBus.Publish(ctx, event); err != nil {
|
||||||
|
s.logger.Error("failed to publish event", zap.Error(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return user, nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 业务指标监控
|
||||||
|
|
||||||
|
**自定义业务指标**:
|
||||||
|
|
||||||
|
```go
|
||||||
|
var (
|
||||||
|
userRegistrations = prometheus.NewCounterVec(
|
||||||
|
prometheus.CounterOpts{
|
||||||
|
Name: "user_registrations_total",
|
||||||
|
Help: "Total number of user registrations",
|
||||||
|
},
|
||||||
|
[]string{"source", "status"},
|
||||||
|
)
|
||||||
|
|
||||||
|
walletBalance = prometheus.NewGaugeVec(
|
||||||
|
prometheus.GaugeOpts{
|
||||||
|
Name: "wallet_balance",
|
||||||
|
Help: "Current wallet balance",
|
||||||
|
},
|
||||||
|
[]string{"user_id", "currency"},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 迁移策略
|
||||||
|
|
||||||
|
### 阶段 1: 基础设施准备 (1-2 周)
|
||||||
|
|
||||||
|
1. 搭建新的开发环境
|
||||||
|
2. 建立 CI/CD 流水线
|
||||||
|
3. 配置监控和日志系统
|
||||||
|
|
||||||
|
### 阶段 2: 用户域重构 (2-3 周)
|
||||||
|
|
||||||
|
1. 重构用户认证服务
|
||||||
|
2. 迁移用户数据
|
||||||
|
3. 更新客户端集成
|
||||||
|
|
||||||
|
### 阶段 3: 金融域重构 (2-3 周)
|
||||||
|
|
||||||
|
1. 重构钱包服务
|
||||||
|
2. 重构支付服务
|
||||||
|
3. 数据一致性验证
|
||||||
|
|
||||||
|
### 阶段 4: 数据服务域重构 (3-4 周)
|
||||||
|
|
||||||
|
1. 重构数据网关
|
||||||
|
2. 重新组织业务 API
|
||||||
|
3. 性能优化
|
||||||
|
|
||||||
|
### 阶段 5: 产品域重构 (1-2 周)
|
||||||
|
|
||||||
|
1. 重构产品管理
|
||||||
|
2. 重构访问控制
|
||||||
|
3. 清理遗留代码
|
||||||
|
|
||||||
|
### 阶段 6: 平台域完善 (1-2 周)
|
||||||
|
|
||||||
|
1. 完善管理后台
|
||||||
|
2. 优化监控系统
|
||||||
|
3. 性能调优
|
||||||
|
|
||||||
|
## 📋 重构检查清单
|
||||||
|
|
||||||
|
### 代码质量
|
||||||
|
|
||||||
|
- [ ] 代码覆盖率 > 80%
|
||||||
|
- [ ] 静态代码分析通过
|
||||||
|
- [ ] API 文档完整
|
||||||
|
- [ ] 性能基准测试
|
||||||
|
|
||||||
|
### 架构质量
|
||||||
|
|
||||||
|
- [ ] 服务间松耦合
|
||||||
|
- [ ] 明确的业务边界
|
||||||
|
- [ ] 数据一致性保证
|
||||||
|
- [ ] 容错和恢复机制
|
||||||
|
|
||||||
|
### 运维质量
|
||||||
|
|
||||||
|
- [ ] 自动化部署
|
||||||
|
- [ ] 监控告警完整
|
||||||
|
- [ ] 日志可追溯
|
||||||
|
- [ ] 备份恢复测试
|
||||||
|
|
||||||
|
### 安全质量
|
||||||
|
|
||||||
|
- [ ] 认证授权机制
|
||||||
|
- [ ] 数据加密传输
|
||||||
|
- [ ] 漏洞扫描通过
|
||||||
|
- [ ] 安全策略文档
|
||||||
|
|
||||||
|
## 🎯 预期收益
|
||||||
|
|
||||||
|
1. **开发效率提升 40%** - 通过标准化和自动化
|
||||||
|
2. **系统可靠性提升 60%** - 通过完善的测试和监控
|
||||||
|
3. **维护成本降低 50%** - 通过清晰的架构和文档
|
||||||
|
4. **新功能交付速度提升 3 倍** - 通过模块化设计
|
||||||
|
|
||||||
|
重构是一个渐进的过程,建议分阶段实施,确保每个阶段都有明确的交付物和验收标准。
|
||||||
399
项目架构文档.md
Normal file
399
项目架构文档.md
Normal file
@@ -0,0 +1,399 @@
|
|||||||
|
# 天远 API 服务器 - 微服务架构文档
|
||||||
|
|
||||||
|
## 项目概述
|
||||||
|
|
||||||
|
天远 API 服务器是一个基于 `go-zero` 框架构建的微服务架构系统,主要提供数据加密传输、用户管理、产品管理、支付等核心业务功能。整个系统采用微服务架构模式,每个服务独立部署,通过 gRPC 和 HTTP 进行通信。
|
||||||
|
|
||||||
|
## 技术栈
|
||||||
|
|
||||||
|
- **微服务框架**: go-zero
|
||||||
|
- **编程语言**: Go
|
||||||
|
- **数据库**: MySQL
|
||||||
|
- **缓存**: Redis
|
||||||
|
- **消息队列**: Kafka (通过 kq)
|
||||||
|
- **服务注册发现**: Etcd
|
||||||
|
- **API 协议**: HTTP/REST + gRPC
|
||||||
|
- **加密**: AES, MD5
|
||||||
|
- **第三方服务**: 阿里云短信、七牛云存储、百度 AI、支付宝支付
|
||||||
|
|
||||||
|
## 整体架构图
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
||||||
|
│ 客户端应用 │ │ 管理端应用 │ │ 第三方系统 │
|
||||||
|
└─────────────────┘ └─────────────────┘ └─────────────────┘
|
||||||
|
│ │ │
|
||||||
|
▼ ▼ ▼
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ API 网关层 │
|
||||||
|
│ (gateway-api) │
|
||||||
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ 微服务集群 │
|
||||||
|
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
||||||
|
│ │ API │ │ User │ │ Sentinel │ │ Admin │ │
|
||||||
|
│ │ Service │ │ Service │ │ Service │ │ Service │ │
|
||||||
|
│ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │
|
||||||
|
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
||||||
|
│ │ MQS │ │ Index │ │ WestDex │ │ 共享包 │ │
|
||||||
|
│ │ Service │ │ Service │ │ Module │ │ (pkg) │ │
|
||||||
|
│ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │
|
||||||
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ 基础设施层 │
|
||||||
|
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
||||||
|
│ │ MySQL │ │ Redis │ │ Kafka │ │ Etcd │ │
|
||||||
|
│ │ Database │ │ Cache │ │MessageQueue │ │Service Disc │ │
|
||||||
|
│ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │
|
||||||
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## 服务详细说明
|
||||||
|
|
||||||
|
### 1. Gateway Service (网关服务)
|
||||||
|
|
||||||
|
**路径**: `apps/gateway/`
|
||||||
|
|
||||||
|
**功能**:
|
||||||
|
|
||||||
|
- 统一入口网关,处理所有外部请求
|
||||||
|
- 用户认证与授权
|
||||||
|
- 企业认证管理
|
||||||
|
- 产品信息查询
|
||||||
|
- 用户产品管理
|
||||||
|
- 充值管理
|
||||||
|
- 白名单管理
|
||||||
|
|
||||||
|
**API 端点**:
|
||||||
|
|
||||||
|
- `/api/console/auth/*` - 认证相关(登录、注册、验证码)
|
||||||
|
- `/api/console/user/*` - 用户管理(用户信息、企业认证)
|
||||||
|
- `/api/console/product/*` - 产品管理
|
||||||
|
- `/api/console/user-product/*` - 用户产品关联
|
||||||
|
- `/api/console/topup/*` - 充值管理
|
||||||
|
- `/api/console/whitelist/*` - 白名单管理
|
||||||
|
|
||||||
|
**依赖**:
|
||||||
|
|
||||||
|
- UserRpc: 用户服务的 gRPC 客户端
|
||||||
|
- SentinelRpc: 哨兵服务的 gRPC 客户端
|
||||||
|
|
||||||
|
### 2. API Service (核心 API 服务)
|
||||||
|
|
||||||
|
**路径**: `apps/api/`
|
||||||
|
|
||||||
|
**功能**:
|
||||||
|
|
||||||
|
- 核心业务 API 处理
|
||||||
|
- 数据加密解密
|
||||||
|
- 第三方接口代理
|
||||||
|
- 业务逻辑处理
|
||||||
|
|
||||||
|
**业务模块**:
|
||||||
|
|
||||||
|
- **IVYZ**: 数据查询服务 (7 个 API 端点)
|
||||||
|
- **FLXG**: 风险评估服务 (12 个 API 端点)
|
||||||
|
- **QYGL**: 企业管理服务 (6 个 API 端点)
|
||||||
|
- **YYSY**: 应用系统服务 (7 个 API 端点)
|
||||||
|
- **JRZQ**: 金融征信服务 (4 个 API 端点)
|
||||||
|
- **COMB**: 组合业务服务 (1 个 API 端点)
|
||||||
|
|
||||||
|
**特点**:
|
||||||
|
|
||||||
|
- 所有 API 都需要通过 `ApiAuthInterceptor` 中间件认证
|
||||||
|
- 数据采用 AES 加密传输
|
||||||
|
- 集成了消息队列服务记录 API 调用
|
||||||
|
|
||||||
|
### 3. User Service (用户服务)
|
||||||
|
|
||||||
|
**路径**: `apps/user/`
|
||||||
|
|
||||||
|
**功能**:
|
||||||
|
|
||||||
|
- 用户注册、登录、认证
|
||||||
|
- 企业认证管理
|
||||||
|
- 钱包服务(余额、充值、扣款)
|
||||||
|
- API 请求记录管理
|
||||||
|
|
||||||
|
**gRPC 服务**:
|
||||||
|
|
||||||
|
- `Auth`: 用户认证服务
|
||||||
|
- `User`: 用户信息服务
|
||||||
|
- `WalletService`: 钱包服务
|
||||||
|
- `Enterprise`: 企业服务
|
||||||
|
- `ApiRequestService`: API 请求记录服务
|
||||||
|
|
||||||
|
**数据模型**:
|
||||||
|
|
||||||
|
- 用户信息
|
||||||
|
- 企业认证信息
|
||||||
|
- 钱包余额
|
||||||
|
- 扣款记录
|
||||||
|
- 充值记录
|
||||||
|
- API 请求记录
|
||||||
|
|
||||||
|
### 4. Sentinel Service (哨兵服务)
|
||||||
|
|
||||||
|
**路径**: `apps/sentinel/`
|
||||||
|
|
||||||
|
**功能**:
|
||||||
|
|
||||||
|
- 产品管理
|
||||||
|
- 用户产品关联管理
|
||||||
|
- 白名单管理
|
||||||
|
- 密钥管理
|
||||||
|
- 支付管理
|
||||||
|
|
||||||
|
**gRPC 服务**:
|
||||||
|
|
||||||
|
- `product`: 产品服务
|
||||||
|
- `userProduct`: 用户产品服务
|
||||||
|
- `whitelist`: 白名单服务
|
||||||
|
- `secret`: 密钥服务
|
||||||
|
- `topUp`: 充值服务
|
||||||
|
|
||||||
|
### 5. Admin Service (管理端服务)
|
||||||
|
|
||||||
|
**路径**: `apps/admin/`
|
||||||
|
|
||||||
|
**功能**:
|
||||||
|
|
||||||
|
- 管理员认证
|
||||||
|
- 企业审核管理
|
||||||
|
- 产品管理
|
||||||
|
- 用户管理
|
||||||
|
- 用户充值
|
||||||
|
|
||||||
|
**API 模块**:
|
||||||
|
|
||||||
|
- `auth`: 管理员登录
|
||||||
|
- `review`: 企业审核
|
||||||
|
- `product`: 产品 CRUD 操作
|
||||||
|
- `user`: 用户管理和充值
|
||||||
|
|
||||||
|
### 6. MQS Service (消息队列服务)
|
||||||
|
|
||||||
|
**路径**: `apps/mqs/`
|
||||||
|
|
||||||
|
**功能**:
|
||||||
|
|
||||||
|
- 消息队列消费者服务
|
||||||
|
- API 请求记录处理
|
||||||
|
- 异步任务处理
|
||||||
|
|
||||||
|
**特点**:
|
||||||
|
|
||||||
|
- 监听 Kafka 消息队列
|
||||||
|
- 处理 API 请求日志
|
||||||
|
- 支持分布式消息处理
|
||||||
|
|
||||||
|
### 7. Index Service (索引服务)
|
||||||
|
|
||||||
|
**路径**: `apps/index/`
|
||||||
|
|
||||||
|
**功能**:
|
||||||
|
|
||||||
|
- 产品索引服务
|
||||||
|
- 搜索功能支持
|
||||||
|
|
||||||
|
### 8. WestDex Module (西部对接模块)
|
||||||
|
|
||||||
|
**路径**: `westDex/`
|
||||||
|
|
||||||
|
**功能**:
|
||||||
|
|
||||||
|
- 第三方西部数据源对接
|
||||||
|
- 数据加密解密
|
||||||
|
- 独立运行的数据处理模块
|
||||||
|
|
||||||
|
## 共享包 (pkg/)
|
||||||
|
|
||||||
|
### 1. crypto
|
||||||
|
|
||||||
|
- AES 加密解密
|
||||||
|
- MD5 哈希
|
||||||
|
- 西部数据源专用加密
|
||||||
|
|
||||||
|
### 2. errs
|
||||||
|
|
||||||
|
- 统一错误处理
|
||||||
|
- API 错误码定义
|
||||||
|
|
||||||
|
### 3. jwt
|
||||||
|
|
||||||
|
- JWT 令牌生成和验证
|
||||||
|
|
||||||
|
### 4. models
|
||||||
|
|
||||||
|
- 消息队列数据模型
|
||||||
|
- 通用数据结构
|
||||||
|
|
||||||
|
### 5. response
|
||||||
|
|
||||||
|
- 统一响应格式
|
||||||
|
|
||||||
|
### 6. generate
|
||||||
|
|
||||||
|
- 支付相关生成器
|
||||||
|
|
||||||
|
### 7. sqlutil
|
||||||
|
|
||||||
|
- 数据库工具函数
|
||||||
|
|
||||||
|
## 中间件系统
|
||||||
|
|
||||||
|
### 1. AuthInterceptor
|
||||||
|
|
||||||
|
- 用户身份认证
|
||||||
|
- JWT 令牌验证
|
||||||
|
- 用户上下文注入
|
||||||
|
|
||||||
|
### 2. ApiAuthInterceptor
|
||||||
|
|
||||||
|
- API 调用认证
|
||||||
|
- 签名验证
|
||||||
|
- 限流控制
|
||||||
|
|
||||||
|
### 3. EntAuthInterceptor
|
||||||
|
|
||||||
|
- 企业认证检查
|
||||||
|
- 企业用户权限验证
|
||||||
|
|
||||||
|
### 4. ApiMqsInterceptor
|
||||||
|
|
||||||
|
- API 调用记录
|
||||||
|
- 消息队列发送
|
||||||
|
|
||||||
|
## 数据库设计
|
||||||
|
|
||||||
|
### 主要数据表
|
||||||
|
|
||||||
|
- `users`: 用户基础信息
|
||||||
|
- `enterprises`: 企业认证信息
|
||||||
|
- `products`: 产品信息
|
||||||
|
- `user_products`: 用户产品关联
|
||||||
|
- `wallets`: 钱包信息
|
||||||
|
- `recharge_records`: 充值记录
|
||||||
|
- `deduction_records`: 扣款记录
|
||||||
|
- `api_requests`: API 请求记录
|
||||||
|
- `pay_orders`: 支付订单
|
||||||
|
- `whitelists`: 白名单
|
||||||
|
- `secrets`: 密钥管理
|
||||||
|
|
||||||
|
## 配置管理
|
||||||
|
|
||||||
|
每个服务都支持多环境配置:
|
||||||
|
|
||||||
|
- `etc/*.yaml`: 生产环境配置
|
||||||
|
- `etc/*.dev.yaml`: 开发环境配置
|
||||||
|
|
||||||
|
**主要配置项**:
|
||||||
|
|
||||||
|
- 数据库连接 (MySQL)
|
||||||
|
- 缓存配置 (Redis)
|
||||||
|
- 服务发现 (Etcd)
|
||||||
|
- 第三方服务 (阿里云、七牛云、百度 AI)
|
||||||
|
- JWT 配置
|
||||||
|
- 消息队列配置
|
||||||
|
|
||||||
|
## 部署架构
|
||||||
|
|
||||||
|
### Docker 化部署
|
||||||
|
|
||||||
|
每个服务都有独立的 Dockerfile:
|
||||||
|
|
||||||
|
- `apps/*/Dockerfile`
|
||||||
|
|
||||||
|
### 服务发现
|
||||||
|
|
||||||
|
- 使用 Etcd 作为服务注册中心
|
||||||
|
- gRPC 服务自动注册和发现
|
||||||
|
|
||||||
|
### 负载均衡
|
||||||
|
|
||||||
|
- 通过 go-zero 内置的负载均衡机制
|
||||||
|
- 支持多种负载均衡策略
|
||||||
|
|
||||||
|
## 安全机制
|
||||||
|
|
||||||
|
### 1. 数据传输安全
|
||||||
|
|
||||||
|
- AES 加密传输
|
||||||
|
- HTTPS 协议
|
||||||
|
- 签名验证
|
||||||
|
|
||||||
|
### 2. 身份认证
|
||||||
|
|
||||||
|
- JWT 令牌认证
|
||||||
|
- 多级权限控制
|
||||||
|
- 白名单机制
|
||||||
|
|
||||||
|
### 3. 数据存储安全
|
||||||
|
|
||||||
|
- 数据库密码加密
|
||||||
|
- 敏感信息脱敏
|
||||||
|
- 访问日志记录
|
||||||
|
|
||||||
|
## 监控与日志
|
||||||
|
|
||||||
|
### 日志系统
|
||||||
|
|
||||||
|
- 结构化日志记录
|
||||||
|
- 分级日志管理
|
||||||
|
- 链路追踪支持
|
||||||
|
|
||||||
|
### 监控指标
|
||||||
|
|
||||||
|
- API 调用统计
|
||||||
|
- 服务健康检查
|
||||||
|
- 性能指标监控
|
||||||
|
|
||||||
|
## 扩展性设计
|
||||||
|
|
||||||
|
### 水平扩展
|
||||||
|
|
||||||
|
- 无状态服务设计
|
||||||
|
- 数据库读写分离支持
|
||||||
|
- 缓存集群支持
|
||||||
|
|
||||||
|
### 模块化设计
|
||||||
|
|
||||||
|
- 微服务独立部署
|
||||||
|
- 插件化业务模块
|
||||||
|
- 模板化代码生成
|
||||||
|
|
||||||
|
## 开发规范
|
||||||
|
|
||||||
|
### 代码结构
|
||||||
|
|
||||||
|
每个微服务遵循 go-zero 标准结构:
|
||||||
|
|
||||||
|
```
|
||||||
|
service/
|
||||||
|
├── internal/
|
||||||
|
│ ├── config/ # 配置定义
|
||||||
|
│ ├── handler/ # HTTP处理器
|
||||||
|
│ ├── logic/ # 业务逻辑
|
||||||
|
│ ├── svc/ # 服务上下文
|
||||||
|
│ ├── types/ # 类型定义
|
||||||
|
│ └── middleware/ # 中间件
|
||||||
|
├── etc/ # 配置文件
|
||||||
|
├── *.api # API定义文件
|
||||||
|
├── *.proto # protobuf定义
|
||||||
|
└── Dockerfile # 容器化配置
|
||||||
|
```
|
||||||
|
|
||||||
|
### API 设计规范
|
||||||
|
|
||||||
|
- RESTful API 设计
|
||||||
|
- 统一错误码
|
||||||
|
- 标准化响应格式
|
||||||
|
- 版本控制支持
|
||||||
|
|
||||||
|
## 总结
|
||||||
|
|
||||||
|
天远 API 服务器采用了现代化的微服务架构,具备良好的可扩展性、可维护性和安全性。通过 go-zero 框架的支持,实现了高性能的服务间通信和统一的开发规范。整个系统支持多环境部署、监控告警、日志追踪等企业级特性,是一个完整的商业级 API 服务平台。
|
||||||
Reference in New Issue
Block a user