迭代方案确认
This commit is contained in:
396
docs/7月迭代/来源材料/业务需求.md
Normal file
396
docs/7月迭代/来源材料/业务需求.md
Normal file
@@ -0,0 +1,396 @@
|
||||
> 本文只保留原始业务需求措辞,不作为技术实施结论。最终口径以 [标准评审稿](../7月迭代技术方案-标准评审稿.md) 为准。
|
||||
|
||||
其中有一些需要对接第三方系统的,除了gateway,都可以划分阶段
|
||||
|
||||
|
||||
|
||||
1.当前所有的卡在后台手动操作复机必须要实名,实际上行业卡应当允许未实名复机
|
||||
2. 用户进入H5后需要先绑定手机号后强制先充值后强制实名。这个功能能否进行后台设置,例如这一批设备需要强制先充值后实名,这一批资产可以先实名后充值?
|
||||
3.店铺列表搜索栏新增一项:联系电话,便于搜索
|
||||
4.操作拦截:若当前资产存在退款申请时(但为通过审批时),该资产不允许操作换货。且出现提示:该资产存在退款申请
|
||||
5. 套餐延续创建时选择购买即生效或实名即生效的条件,同时在套餐分配时提供修改条件的功能,并以变更后的条件为最终版本,已经分配出去的套餐不会被后续的宿主套餐修改影响,需要回收后重新分配才能生效
|
||||
6. 在资产详情页增加所有待生效套餐加上生效套餐加起来的最后到期时间
|
||||
7.lot卡管理和设备管理新增已实名/未实名的筛选查询条件
|
||||
8.设备批量分配代理和套餐系列:因设备号不是连号,故需要提供导入excel表的方式进行批量分配代理和套餐系列。excle表头为:设备号
|
||||
|
||||
> 评审结论:拆成“批量分配代理”和“批量分配套餐系列”两个独立命令、两个前端入口和两个任务类型;可复用 Excel 解析与任务基础设施,但单个任务不得同时修改两个字段。
|
||||
9.C端支付时,卡资产只允许支付宝支付以及钱包支付,如果用微信支付就拒绝,设备只允许微信支付以及钱包支付,如果用支付宝支付就拒绝
|
||||
10.限速规则:根据不同运营商限速规则,基于套餐流量设置不同的卡/设备的限速规则,限速接口由gateway提供
|
||||
|
||||
> 技术口径说明:Gateway 只支持按 `cardNo` 限速,不存在设备级限速。单卡直接使用 ICCID;设备场景先查 `tb_device_sim_binding.is_current=true` 的当前卡,再使用该卡 ICCID。取消限速仍重复调用同一个限速接口,只是发送取消参数。本期仅提供后台手动设置/取消,内部单位为 `kbps`;不做套餐字段和自动限速规则。
|
||||
|
||||
11. 资产信息详情字段新增:资产信息页面卡信息/设备信息板块,将当前生效套餐的过期时间作为一个字段显示且套餐还剩15天到期时该字段高亮显示。
|
||||
12. 换货管理:
|
||||
| 编号 | 需求 |
|
||||
| ------- | ---------------------------------------------------- |
|
||||
| EXC-001 | 修正换货列表旧资产标识和新资产标识显示混乱问题。 |
|
||||
| EXC-002 | 换货列表中旧资产标识符和新资产标识符均显示为 ICCID。 |
|
||||
| EXC-003 | 旧资产查询支持 ICCID、接入号、虚拟号。 |
|
||||
| EXC-004 | 新资产查询支持 ICCID、接入号、虚拟号。 |
|
||||
13.列表字段新增:
|
||||
| 编号 | 模块 | 新增字段 |
|
||||
| ------- | ------------ | -------------- |
|
||||
| COL-001 | 退款管理列表 | 提交人、审批人 |
|
||||
| COL-002 | 代理充值列表 | 提交人、审批人 |
|
||||
| COL-003 | 换号管理列表 | 提交人 |
|
||||
14. 导出功能:
|
||||
## 6.8.1 lot 卡导出
|
||||
|
||||
### 支持套餐临期30天内所有资产的导出。字段按照卡/设备的导出表进行导出。
|
||||
|
||||
| 编号 | 需求 |
|
||||
| -------- | ---------------------------- |
|
||||
| EXPD-001 | lot 卡导出字段新增套餐名称。 |
|
||||
| EXPD-002 | lot 卡导出字段新增使用流量。 |
|
||||
| EXPD-003 | lot 卡导出字段新增剩余流量。 |
|
||||
|
||||
## 6.8.2 代理资金概况-预充值钱包流水导出
|
||||
|
||||
导出字段:
|
||||
|
||||
| 字段 | 说明 |
|
||||
| ------------------- | ------------------------------------------------------------ |
|
||||
| 店铺名称 | 代理店铺名称 |
|
||||
| 交易类型 | 充值、扣款、退款等 |
|
||||
| 交易金额 | 以元为单位 |
|
||||
| 状态 | 交易状态 |
|
||||
| 资产类型 | 卡/设备等 |
|
||||
| 资产标识 | ICCID/设备号等 |
|
||||
| 交易时间 | 流水生成时间 |
|
||||
| 交易前金额 | 交易前余额 |
|
||||
| 交易后金额 | 交易后余额 |
|
||||
| 购买套餐名称 | 资产此条扣款记录对应的套餐名称 |
|
||||
| 操作人 | 明确交易执行主体:代理账号 / 平台账号(明确账号名称) |
|
||||
| 交易 ID | 每一笔流水的全局唯一主键,彻底避免重复流水、对账串号、精准定位单条交易 |
|
||||
| 关联业务订单号 | 充值、扣款、退款等交易对应的原始业务订单编号 |
|
||||
| 交易渠道 / 支付方式 | 明确交易来源:余额支付等 |
|
||||
|
||||
## 6.8.3 套餐列表导出
|
||||
|
||||
导出字段:
|
||||
|
||||
| 字段 |
|
||||
| -------------- |
|
||||
| 套餐编码 |
|
||||
| 套餐名称 |
|
||||
| 套餐系列名称 |
|
||||
| 套餐类型 |
|
||||
| 套餐时长(月) |
|
||||
| 套餐时长说明 |
|
||||
| 套餐周期类型 |
|
||||
| 套餐天数 |
|
||||
| 真流量额度(MB) |
|
||||
| 虚流量额度(MB) |
|
||||
| 是否启用虚流量 |
|
||||
| 虚流量比例 |
|
||||
| 流量重置周期 |
|
||||
| 到期时间基准 |
|
||||
| 成本价(元) |
|
||||
| 建议售价(元) |
|
||||
| 价格配置状态 |
|
||||
| 状态 |
|
||||
| 上架状态 |
|
||||
| 是否赠送套餐 |
|
||||
| 创建人ID |
|
||||
| 更新人ID |
|
||||
| 创建时间 |
|
||||
| 更新时间 |
|
||||
| 删除时间 |
|
||||
|
||||
## 6.8.4 退款管理退款列表导出
|
||||
|
||||
导出字段:
|
||||
|
||||
| 字段 |
|
||||
| ---------------- |
|
||||
| 退款单号 |
|
||||
| 代理店铺名称 |
|
||||
| 关联的支付订单号 |
|
||||
| 资产类型 |
|
||||
| 资产标识 |
|
||||
| 套餐名称 |
|
||||
| 原订单金额 |
|
||||
| 实收金额 |
|
||||
| 可退金额 |
|
||||
| 申请退款金额 |
|
||||
| 实际退款金额 |
|
||||
| 退款到账方式 |
|
||||
| 状态 |
|
||||
| 退款原因 |
|
||||
| 备注 |
|
||||
| 审批备注 |
|
||||
| 退款申请时间 |
|
||||
| 退款完成时间 |
|
||||
| 提交人 |
|
||||
| 部门领导审批人 |
|
||||
| 财务审批人 |
|
||||
| 退款凭证 |
|
||||
|
||||
## 6.8.5 换货管理导出
|
||||
|
||||
### C端客户有自己的唯一标识码。换货管理可以针对该C端客户记录该客户换过几次设备或卡。同时资产本身也做换货标识。
|
||||
|
||||
导出字段:
|
||||
|
||||
| 字段 |
|
||||
| ------------ |
|
||||
| 换货单号 |
|
||||
| 换货类型 |
|
||||
| 换货原因 |
|
||||
| 问题描述 |
|
||||
| 旧资产类型 |
|
||||
| 旧资产标识符 |
|
||||
| 新资产标识符 |
|
||||
| 收货人姓名 |
|
||||
| 收货人电话 |
|
||||
| 收货地址 |
|
||||
| 快递公司 |
|
||||
| 快递单号 |
|
||||
| 状态 |
|
||||
| 创建人 |
|
||||
| 创建时间 |
|
||||
|
||||
## 6.8.6 代理充值导出
|
||||
|
||||
导出字段:
|
||||
|
||||
| 字段 |
|
||||
| -------------- |
|
||||
| 充值单号 |
|
||||
| 店铺名称 |
|
||||
| 充值类型 |
|
||||
| 充值金额 |
|
||||
| 实付金额 |
|
||||
| 充值前余额 |
|
||||
| 充值后余额 |
|
||||
| 状态 |
|
||||
| 支付方式 |
|
||||
| 支付通道 |
|
||||
| 运营备注 |
|
||||
| 驳回原因 |
|
||||
| 创建时间 |
|
||||
| 支付时间 |
|
||||
| 完成时间 |
|
||||
| 提交人 |
|
||||
| 部门领导审批人 |
|
||||
| 财务审批人 |
|
||||
| 支付凭证 |
|
||||
| 备注 |
|
||||
|
||||
| 套餐时长说明 | :这是剩余天数
|
||||
|
||||
| 退款到账方式 |
|
||||
|
||||
这个好像没有 :
|
||||
|
||||
### 之后是否需要增加?因加入财务审批操作退款,可能有原路退回或不同的退款方式如支付宝退款或微信退款?
|
||||
|
||||
| 部门领导审批人 |
|
||||
|
||||
| 财务审批人 |
|
||||
|
||||
这两个好像也没有
|
||||
|
||||
### 目前是没有呀,但是之前不是说了审批流程需要加上吗?列表字段同时需要新增
|
||||
|
||||
| 支付通道 |
|
||||
|
||||
这是啥玩意
|
||||
|
||||
### 去掉
|
||||
|
||||
"导出功能可以根据不同权限显示的字段不同且可以自己选择需要导出的字段。像现在这样全量导出,大家都可以看到虚流量等不想让所有人都看到的字段。" 什么叫不同权限,这个不同权限显示字段不同是固定的吗,平台就是固定的,代理就是固定的等等,如果是这样的话需要标记对应的导出字段的权限划分
|
||||
|
||||
### 因为不同的角色,权限不一致,列表的字段不能给每个用户看到类似真流量这样的字段,比如客服只能看到虚流量跟客户看到的一样,应当在角色管理中新增一个导出字段配置
|
||||
|
||||
> 评审结论:角色级导出字段配置属于本期范围。后端返回当前角色允许导出的字段,最终导出字段取“角色授权字段”与“用户本次选择字段”的交集,前端不能绕过服务端授权。
|
||||
|
||||
|
||||
15. 套餐相关: 套餐下架后,正在使用该套餐的客户仍可续费。,下架套餐续费仅支持客户自己购买。 ,下架套餐不可被新购买。
|
||||
16. 代理分销码与佣金提现
|
||||
|
||||
> 迭代范围变更(2026-07-14):需求 16 整体移出 7 月迭代,后续独立立项和评审。以下内容仅保留原始需求记录,本期不开发、不迁移、不发布。
|
||||
|
||||
### 员工可作为代理发展人进行标识。(在系统中如何体现?)
|
||||
|
||||
| 编号 | 需求 |
|
||||
| ------- | ---------------------------------------------------- |
|
||||
| DST-001 | 新建代理时自动建立分销归属关系。 |
|
||||
| DST-002 | 员工可作为代理发展人进行标识。(在系统中如何体现?) |
|
||||
| DST-003 | 佣金提现前,代理必须签署合同。(必填) |
|
||||
| DST-004 | 佣金提现需上传营业执照。(可选) |
|
||||
| DST-005 | 佣金提现需上传法人身份证。(必填) |
|
||||
| DST-006 | 佣金提现可选上传门头照。(可选) |
|
||||
| DST-007 | 佣金提现需上传发票,且公司主体需与合同一致。(可选) |
|
||||
|
||||
> 当前结论:原技术方案不再属于 7 月迭代范围。后续重新立项时再评审分销关系、代理申请审批、开店幂等和提现材料。
|
||||
|
||||
17. 不同渠道信用额度,钱包支持信用额度支付
|
||||
| 编号 | 需求 |
|
||||
| ------- | ------------------------------------------------------------ |
|
||||
| BPO-009 | 新建代理时新增“是否可授权额度”开关。平台用户账号默认拥有授权额度。 |
|
||||
| BPO-010 | 不同代理可设置不同额度下限。平台用户可根据不同的角色设置不同的额度下限。 |
|
||||
| BPO-011 | 授权额度用于订购套餐、代理余额充值等需要涉及金额的所有模块。 |
|
||||
| BPO-012 | 授权额度代理可显示负数余额。平台用户也可显示负数余额。 |
|
||||
|
||||
> 评审结论:信用额度只属于代理主钱包。平台员工是操作主体而不是结算主体,不建立员工钱包、不配置员工信用额度,也不展示员工负余额。客户角色可以配置以后新建店铺的默认额度;修改角色不更新已有店铺,已有店铺只能在店铺资金页面直接调整实际额度。
|
||||
|
||||
18. 系统原先对于审核都是单人审核,现在希望增加多人审批以及相关设置
|
||||
| 编号 | 需求 |
|
||||
| ------- | ------------------------------------------------------------ |
|
||||
| APR-001 | 代理可在系统提交充值申请。 |
|
||||
| APR-002 | 提交充值申请后系统展示收款二维码,代理扫码支付。 |
|
||||
| APR-003 | 充值和退款均支持多级审核。 |
|
||||
| APR-004 | 审核环节包括提交人部门领导审核和财务审核。 |
|
||||
| APR-005 | 待审核订单需有消息提示。 |
|
||||
| APR-006 | 审核提醒按流程环节触发,上一审批人完成审批后才提示下一审批人。 |
|
||||
| APR-007 | 审核通过后通知申请人。 |
|
||||
| APR-008 | 审核驳回后通知申请人,并附带驳回原因。 |
|
||||
| APR-009 | 审核流程需对接企业微信审批流程。 |
|
||||
|
||||
> 技术口径说明:当前系统没有部门组织模型。“部门领导审核、财务审核”作为默认流程节点名称处理,实际审批人由流程定义配置的角色或指定账号产生,不根据提交人部门自动推导,也不在代码中固定角色名称。每次通过、驳回、退回都形成不可修改的审批意见记录,并可附带最多 5 个审批附件;驳回和退回意见必填。
|
||||
|
||||
> 审批详情必须展示业务单号、提交人、审批关键字段、业务资料、此前审批人的意见和审批附件。业务资料与审批附件分开存储和展示;流程启动时固化业务快照,实时业务页仍按原业务数据范围校验。
|
||||
|
||||
19.批量订购套餐
|
||||
内部员工进入批量订购页面。
|
||||
2. 选择代理、ICCID号段/设备号、订购套餐。或上传 Excel 文件,资产标识支持 ICCID/设备号
|
||||
3. Excel表头:跳转至6.3会显示。
|
||||
4. 系统校验导入文件和资产状态。
|
||||
5. 校验通过的资产从代理余额扣款并完成订购。
|
||||
6. 校验失败的明细在页面展示失败原因。
|
||||
7. 系统记录操作员、导入明细、导入数量和导入时间。
|
||||
|
||||
| 编号 | 需求 |
|
||||
| ------- | ---------------------------------------------------- |
|
||||
| BPO-001 | 批量订购由内部员工操作。 |
|
||||
| BPO-002 | 支持代理自行充值钱包后,由员工批量订购并从余额扣款。 |
|
||||
| BPO-003 | 支持员工代充值至代理余额后,再批量订购并从余额扣款。 |
|
||||
| BPO-004 | 批量订购无需审核。 |
|
||||
| BPO-005 | 资产标识支持 ICCID/设备号。 |
|
||||
| BPO-006 | 支持 Excel 模板导入。 |
|
||||
| BPO-007 | 需记录操作员、导入明细、导入数量和导入时间。 |
|
||||
| BPO-008 | 导入失败明细需展示在页面,并显示失败原因。 |
|
||||
|
||||
excel表导入字段:
|
||||
| 字段 |
|
||||
| ----------------------------- |
|
||||
| 资产类型 |
|
||||
| 资产标识 |
|
||||
| 套餐系列名称 |
|
||||
| 套餐名称 |
|
||||
| 代理名称 |
|
||||
| 支付方式:代理商账户/员工账户 |
|
||||
|
||||
excel表导入字段修改为以下:
|
||||
|
||||
| 字段 |
|
||||
|
||||
| ----------------------------- |
|
||||
|
||||
| 资产类型 |
|
||||
|
||||
| 资产标识 |
|
||||
|
||||
| 套餐编码 |
|
||||
|
||||
| 套餐名称|
|
||||
|
||||
| 支付方式:线下支付/代理钱包支付 |
|
||||
|
||||
如果用批量订购应当上传对应凭证
|
||||
|
||||
> 评审结论:支付方式按整批统一。页面选择“线下支付”或“代理钱包支付”,Excel 不再包含支付方式列;线下支付按整批上传凭证,混合支付必须拆成不同批次。
|
||||
|
||||
20.退款审批
|
||||
(审批均可通过企微进行提醒和显示并将最新状态同步至卡管)
|
||||
1. 员工提交退款申请。
|
||||
2. 退款单进入多级审核流程。
|
||||
3. 按部门领导、财务顺序审批。
|
||||
4. 当前环节审批完成后,下一环节审批人收到消息提示。
|
||||
5. 审批通过或驳回后通知申请人。
|
||||
|
||||
> 评审结论:微信、支付宝和线下退款由财务在系统外人工完成后确认;本期不接入第三方自动退款。代理钱包支付的退款仅自动回退原扣款代理主钱包,客户资产钱包不在本期自动回退范围。
|
||||
21. 充值审核流程
|
||||
代理自己充值:
|
||||
1. 代理在系统提交充值申请。
|
||||
2. 系统展示收款二维码。
|
||||
3. 代理扫码支付。
|
||||
|
||||
员工代充值:(审批均可通过企微进行提醒和显示并将最新状态同步至卡管)
|
||||
1. 充值单进入多级审核流程。
|
||||
2. 提交人部门领导先审批。
|
||||
3. 财务在上一审批人通过后收到待办提醒并审批。
|
||||
4. 审批通过后通知申请人。
|
||||
5. 审批驳回后通知申请人,并展示驳回原因。
|
||||
|
||||
> 技术口径说明:审批通过只表示审批结论成立,不表示退款已经到账或充值已经入账。退款、充值分别维护业务处理状态并支持异步重试。此次采用停机发布,旧退款业务单审批接口和线下充值 `offline-pay/reject` 不保留兼容窗口。
|
||||
|
||||
22. 套餐临期提醒
|
||||
1. 系统每日计算卡/设备套餐剩余有效期。
|
||||
2. 命中临期规则后生成临期数据。临期提醒规则:按 15 天、7 天、3 天节点分别进行不同方式的提醒。
|
||||
3. 企业客户场景:按 15 天、7 天、3 天节点生成临期列表,并通过企业微信推送给对应业务员。
|
||||
4. 代理端场景(展示):首页展示卡、设备临期数量;资产列表按临期天数高亮。
|
||||
5. C 端场景(展示):公众号首页在套餐剩余有效期小于或等于 15天时展示续费提醒,并显示立即续费按钮。
|
||||
6. 当资产续费成功或不再满足临期条件时,临期提醒自动取消。
|
||||
## 6.1.1 企业客户临期提醒
|
||||
| 编号 | 需求 |
|
||||
| ------- | ------------------------------------------------------------ |
|
||||
| EXP-001 | 后台每日生成企业客户临期列表。 |
|
||||
| EXP-002 | 临期节点包括 15 天、7 天、3 天。 |
|
||||
| EXP-003 | 系统需对接企业微信,将临期列表推送给对应业务员。 |
|
||||
| EXP-004 | 同一资产在不同临期节点可重复触发对应节点提醒,但同一节点每日不可重复推送给同一业务员。 |
|
||||
|
||||
## 6.1.2 代理端临期提醒
|
||||
|
||||
| 编号 | 需求 |
|
||||
| ------- | ------------------------------------------------------------ |
|
||||
| EXP-005 | 代理端首页分别展示临期卡数量和临期设备数量。 |
|
||||
| EXP-006 | 代理端资产列表对临期资产进行颜色标记。 |
|
||||
| EXP-007 | 剩余 15 天标记为粉色,剩余 7 天标记为紫色,剩余 3 天标记为红色。 |
|
||||
| EXP-008 | 剩余 3 天资产在列表中置顶优先展示。 |
|
||||
|
||||
## 6.1.3 C 端公众号首页提醒
|
||||
|
||||
| 编号 | 需求 |
|
||||
| ------- | ------------------------------------------------------------ |
|
||||
| EXP-009 | 当客户套餐剩余有效期小于或等于 15 天时,公众号首页展示套餐到期提醒。 |
|
||||
| EXP-010 | 提醒展示卡号/设备号、剩余有效期和续费引导文案。 |
|
||||
| EXP-011 | 按钮文案为“立即续费”。 |
|
||||
| EXP-012 | 剩余天数需要按日期每天自动更新。 |
|
||||
| EXP-013 | 当套餐已续费或不再满足临期条件时,提醒不再展示。 |
|
||||
推荐展示文案:
|
||||
您的套餐即将到期
|
||||
|
||||
卡号/设备号:xxx
|
||||
剩余有效期:xx 天
|
||||
|
||||
为避免到期后影响正常使用,请您提前完成续费。
|
||||
|
||||
当一个资产拥有排队中的套餐时不属于临期,不参与临期提醒
|
||||
|
||||
后台管理只需要在列表检索中新增临期时间字段 条件为小于等于15天的
|
||||
|
||||
后台管理中资产详情需要新增一个字段临期时间从15天开始显示,前端应当高亮或者变红
|
||||
|
||||
后台管理中资产列表需要新增返回字段,套餐还有多少天过期
|
||||
|
||||
临期列表页面需要确认列表展示什么,检索有什么,导出要什么
|
||||
|
||||
##### 展示:资产标识、资产类型、店铺、套餐名称、剩余天数、到期时间、资产状态、已用流量、剩余流量
|
||||
|
||||
##### 检索条件:资产标识、资产类型、套餐名称、到期时间范围、剩余天数、店铺
|
||||
|
||||
##### 导出:资产标识、资产类型、店铺名称、套餐名称、套餐到期时间、剩余天数、资产状态、已用流量、剩余流量
|
||||
|
||||
临期规则
|
||||
企业客户 15,7,3天
|
||||
代理 15,7,3天
|
||||
C端公众号 小于等于15天
|
||||
|
||||
颜色规则
|
||||
临期天数小于等于15天时,显示粉红色;
|
||||
临期天数小于等于7天时,显示紫色;
|
||||
临期天数小于等于3天时,显示黄色;
|
||||
|
||||
> 最终评审口径:资产临期按当前及排队主套餐的预计最终到期时间计算;3天内改为红色,只在临期独立列表置顶。七月迭代不发送企业微信临期消息,只发送站内通知。
|
||||
729
docs/7月迭代/来源材料/新需求.md
Normal file
729
docs/7月迭代/来源材料/新需求.md
Normal file
@@ -0,0 +1,729 @@
|
||||
> 本文保留新增需求讨论和示例代码,不作为技术实施结论。最终口径以 [标准评审稿](../7月迭代技术方案-标准评审稿.md) 及对应新增需求独立稿为准。
|
||||
|
||||
处理回调的代码
|
||||
```golang
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
ranNumLib "math/rand"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// XML请求体结构
|
||||
type ContractRoot struct {
|
||||
XMLName xml.Name `xml:"ContractRoot"`
|
||||
Type string `xml:"TYPE"`
|
||||
GroupTransactionID string `xml:"GROUP_TRANSACTIONID"`
|
||||
StatusInfo string `xml:"STATUSINFO"`
|
||||
AccNbr string `xml:"ACCNBR"`
|
||||
ICCID string `xml:"ICCID"`
|
||||
SendDt string `xml:"SENDDT"`
|
||||
AcceptType string `xml:"ACCEPTTYPE"`
|
||||
AcceptMsg string `xml:"ACCEPTMSG"`
|
||||
StatusDt string `xml:"STATUSDT"`
|
||||
ResultMsg string `xml:"RESULTMSG"`
|
||||
}
|
||||
|
||||
// 第三方推送数据结构
|
||||
type PushData struct {
|
||||
Msg string `json:"msg"`
|
||||
Code int `json:"code"`
|
||||
Data struct {
|
||||
RequestID string `json:"requestId"`
|
||||
RealStatus bool `json:"realStatus"`
|
||||
ICCID string `json:"iccid"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
func parseCallback(jsonStr []byte) (string, string, error) {
|
||||
// 定义匿名结构体用于解析外层JSON
|
||||
var cb struct {
|
||||
Data string `json:"data"`
|
||||
}
|
||||
err := json.Unmarshal(jsonStr, &cb)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to unmarshal outer JSON: %v", err)
|
||||
}
|
||||
|
||||
// 定义匿名结构体用于解析内层数据
|
||||
var inner struct {
|
||||
DateChanged string `json:"dateChanged"`
|
||||
ICCID string `json:"iccid"`
|
||||
}
|
||||
err = json.Unmarshal([]byte(cb.Data), &inner)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to unmarshal inner data JSON: %v", err)
|
||||
}
|
||||
|
||||
return inner.ICCID, inner.DateChanged, nil
|
||||
}
|
||||
|
||||
// 5GCMP实名后推送到第三方平台
|
||||
func pushToThirdParty(iccid string) (string, error) {
|
||||
// 构建推送数据
|
||||
pushData := PushData{
|
||||
Msg: "查询成功",
|
||||
Code: 200,
|
||||
}
|
||||
pushData.Data.RequestID = uuid.New().String()
|
||||
pushData.Data.RealStatus = true
|
||||
pushData.Data.ICCID = iccid
|
||||
|
||||
// 序列化为JSON
|
||||
jsonData, err := json.Marshal(pushData)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("序列化推送数据失败: %v", err)
|
||||
}
|
||||
|
||||
// 发送HTTP POST请求
|
||||
pushURL := "http://jh.whjhft.com/gswlpushapi/recv.do?type=3"
|
||||
client := &http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
}
|
||||
|
||||
resp, err := client.Post(pushURL, "application/json", bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("HTTP请求失败: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 读取响应
|
||||
respBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("读取响应失败: %v", err)
|
||||
}
|
||||
|
||||
return string(respBody), nil
|
||||
}
|
||||
|
||||
// 获取日志文件路径
|
||||
func getLogFilePath() string {
|
||||
// 创建logs目录
|
||||
logsDir := "logs"
|
||||
if err := os.MkdirAll(logsDir, 0755); err != nil {
|
||||
log.Printf("创建日志目录失败: %v", err)
|
||||
}
|
||||
|
||||
// 按日期生成文件名
|
||||
dateStr := time.Now().Format("2006-01-02")
|
||||
fileName := fmt.Sprintf("5gcmp_callback_%s.log", dateStr)
|
||||
return filepath.Join(logsDir, fileName)
|
||||
}
|
||||
|
||||
func GetQcRandNum() (ranNum string) {
|
||||
randomFloat := ranNumLib.Float64()
|
||||
if randomFloat < 0.5 {
|
||||
randomFloat = 1 - randomFloat
|
||||
}
|
||||
ranNum = fmt.Sprintf("%.16f", randomFloat)
|
||||
return
|
||||
}
|
||||
|
||||
// 写入日志
|
||||
func writeLog(content string) {
|
||||
logFile := getLogFilePath()
|
||||
|
||||
// 打开或创建日志文件
|
||||
file, err := os.OpenFile(logFile, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
log.Printf("打开日志文件失败: %v", err)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// 写入日志内容
|
||||
if _, err := file.WriteString(content); err != nil {
|
||||
log.Printf("写入日志失败: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 向管理平台发送删除实名请求
|
||||
func DelRealName(iccid string) (res string) {
|
||||
account := Account{UserName: "18627991016", Password: "y123456"}
|
||||
sessionid := account.Login()
|
||||
realnameId := getRealnameIdByIccid(iccid, sessionid)
|
||||
if realnameId == "" {
|
||||
return
|
||||
}
|
||||
log.Printf("sessinid:%s,realnameId:%s", sessionid, realnameId)
|
||||
url := "http://jh.whjhft.com/realnamerecord/deleteById.do?responseFunction=initUpdate&id=" + realnameId + "&rfm=" + GetQcRandNum()
|
||||
data := fmt.Sprintf("status=1&iccidMark=%s", iccid)
|
||||
req, err := http.NewRequest("POST", url, strings.NewReader(data))
|
||||
if err != nil {
|
||||
log.Printf("创建请求失败: %v", err)
|
||||
return ""
|
||||
}
|
||||
client := &http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
}
|
||||
req.Header.Set("Cookie", fmt.Sprintf("JSESSIONID=%s", sessionid))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8")
|
||||
req.Header.Set("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.37 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36 Edg/123.0.0.0")
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
log.Printf("HTTP请求失败: %v", err)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
respBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
log.Printf("读取响应失败: %v", err)
|
||||
return
|
||||
}
|
||||
log.Printf("删除实名响应: %s", respBody)
|
||||
res = string(respBody)
|
||||
return
|
||||
}
|
||||
|
||||
func getRealnameIdByIccid(iccid, sessionid string) (realnameId string) {
|
||||
url := "http://jh.whjhft.com/realnamerecord/grid.do?responseFunction=grid&pageSize=15&pageNo=1&rfm=0." + GetQcRandNum()
|
||||
client := &http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
}
|
||||
|
||||
data := fmt.Sprintf("status=1&iccidMark=%s", iccid)
|
||||
|
||||
req, err := http.NewRequest("POST", url, strings.NewReader(data))
|
||||
if err != nil {
|
||||
log.Printf("创建请求失败: %v", err)
|
||||
return
|
||||
}
|
||||
req.Header.Set("Cookie", fmt.Sprintf("JSESSIONID=%s", sessionid))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8")
|
||||
req.Header.Set("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.37 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36 Edg/123.0.0.0")
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
log.Printf("HTTP请求失败: %v", err)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
respBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
log.Printf("读取响应失败: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
var JSONData struct {
|
||||
Code string `json:"code"`
|
||||
Data struct {
|
||||
PageNo int `json:"pageNo"`
|
||||
PageCount int `json:"pageCount"`
|
||||
PageSize int `json:"pageSize"`
|
||||
PageStartOffset int `json:"pageStartOffset"`
|
||||
Total int `json:"total"`
|
||||
Rows []struct {
|
||||
MybatisRecordCount int `json:"mybatisRecordCount"`
|
||||
OrderNo string `json:"orderNo"`
|
||||
JSONUpdateFlag string `json:"jsonUpdateFlag"`
|
||||
ID string `json:"id"`
|
||||
IccidMark string `json:"iccidMark"`
|
||||
Phone string `json:"phone"`
|
||||
AccountID string `json:"accountId"`
|
||||
AccountName string `json:"accountName"`
|
||||
Status int `json:"status"`
|
||||
CreateName string `json:"createName"`
|
||||
CreateDate string `json:"createDate"`
|
||||
StatusStr string `json:"statusStr"`
|
||||
} `json:"rows"`
|
||||
Framework string `json:"framework"`
|
||||
Data string `json:"data"`
|
||||
Count int `json:"count"`
|
||||
Limit int `json:"limit"`
|
||||
Page int `json:"page"`
|
||||
Layui bool `json:"layui"`
|
||||
} `json:"data"`
|
||||
CurrentSessionUserResourceIdsIndex []string `json:"current_session_user_resource_ids_index"`
|
||||
AppResultKey string `json:"app_result_key"`
|
||||
SystemResultKey string `json:"system_result_key"`
|
||||
}
|
||||
json.Unmarshal(respBody, &JSONData)
|
||||
if JSONData.AppResultKey == "0" && JSONData.SystemResultKey == "0" && JSONData.Data.Count > 0 {
|
||||
realnameId = JSONData.Data.Rows[0].ID
|
||||
}
|
||||
|
||||
log.Printf("响应体: %s", respBody)
|
||||
return
|
||||
}
|
||||
|
||||
func getIccidByMsisdn(msisdn, sessionid string) (iccid string) {
|
||||
url := "http://jh.whjhft.com/realnamerecord/grid.do?responseFunction=grid&pageSize=15&pageNo=1&rfm=" + GetQcRandNum()
|
||||
client := &http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
}
|
||||
|
||||
data := fmt.Sprintf("status=1&phone=%s", msisdn)
|
||||
|
||||
req, err := http.NewRequest("POST", url, strings.NewReader(data))
|
||||
if err != nil {
|
||||
log.Printf("创建请求失败: %v", err)
|
||||
return
|
||||
}
|
||||
req.Header.Set("Cookie", fmt.Sprintf("JSESSIONID=%s", sessionid))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8")
|
||||
req.Header.Set("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.37 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36 Edg/123.0.0.0")
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
log.Printf("HTTP请求失败: %v", err)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
respBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
log.Printf("读取响应失败: %v", err)
|
||||
return
|
||||
}
|
||||
var JSONData struct {
|
||||
Code string `json:"code"`
|
||||
Data struct {
|
||||
Rows []struct {
|
||||
IccidMark string `json:"iccidMark"`
|
||||
} `json:"rows"`
|
||||
Count int `json:"count"`
|
||||
} `json:"data"`
|
||||
AppResultKey string `json:"app_result_key"`
|
||||
SystemResultKey string `json:"system_result_key"`
|
||||
}
|
||||
json.Unmarshal(respBody, &JSONData)
|
||||
if JSONData.AppResultKey == "0" && JSONData.SystemResultKey == "0" && JSONData.Data.Count > 0 {
|
||||
iccid = JSONData.Data.Rows[0].IccidMark
|
||||
}
|
||||
log.Printf("响应体: %s", respBody)
|
||||
return
|
||||
}
|
||||
|
||||
func ModifyDate(iccid, dateChanged string) {
|
||||
var jsonData = map[string]interface{}{
|
||||
"iccid": iccid,
|
||||
"dateChanged": dateChanged,
|
||||
}
|
||||
jsonDataBs, _ := json.Marshal(jsonData)
|
||||
|
||||
// 创建请求
|
||||
req, err := http.NewRequest("POST", "http://127.0.0.1:3000/api/v1/inventory/realname/inner_callback", bytes.NewReader(jsonDataBs))
|
||||
if err != nil {
|
||||
writeLog(fmt.Sprintf("创建请求失败:[%s] [%s]实名时间[%s]失败\r\n", "http://127.0.0.1:3000/api/v1/inventory/realname/inner_callback", iccid, dateChanged))
|
||||
return
|
||||
}
|
||||
|
||||
// 设置Content-Type为x-www-form-urlencoded
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
// 发送请求
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
writeLog(fmt.Sprintf("请求接口:[%s] [%s]实名时间[%s]失败\r\n", "http://127.0.0.1:3000/api/v1/inventory/realname/inner_callback", iccid, dateChanged))
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 读取响应内容
|
||||
respBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
writeLog(fmt.Sprintf("请求接口:[%s] [%s]实名时间[%s]失败\r\n", "http://127.0.0.1:3000/api/v1/inventory/realname/inner_callback", iccid, dateChanged))
|
||||
return
|
||||
}
|
||||
|
||||
// 检查响应状态
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
fmt.Printf("请求失败,状态码: %d, 响应: %s\n", resp.StatusCode, string(respBody))
|
||||
writeLog(fmt.Sprintf("请求接口:[%s] [%s]实名时间[%s]失败\r\n", "http://127.0.0.1:3000/api/v1/inventory/realname/inner_callback", iccid, dateChanged))
|
||||
return
|
||||
}
|
||||
|
||||
writeLog(fmt.Sprintf("请求接口:[%s] [%s]实名时间[%s]成功,[%s]\r\n", "http://127.0.0.1:3000/api/v1/inventory/realname/inner_callback", iccid, dateChanged, string(respBody)))
|
||||
|
||||
}
|
||||
|
||||
// 处理回调请求
|
||||
func handleCallback(w http.ResponseWriter, r *http.Request) {
|
||||
// 记录请求开始时间
|
||||
startTime := time.Now()
|
||||
timestamp := startTime.Format("2006-01-02 15:04:05")
|
||||
|
||||
// 构建日志内容
|
||||
var logBuilder strings.Builder
|
||||
logBuilder.WriteString("\n========================================\n")
|
||||
logBuilder.WriteString(fmt.Sprintf("请求时间: %s\n", timestamp))
|
||||
logBuilder.WriteString(fmt.Sprintf("请求方法: %s\n", r.Method))
|
||||
logBuilder.WriteString(fmt.Sprintf("完整URL: %s\n", r.URL.String()))
|
||||
logBuilder.WriteString(fmt.Sprintf("请求路径: %s\n", r.URL.Path))
|
||||
logBuilder.WriteString(fmt.Sprintf("查询参数: %s\n", r.URL.RawQuery))
|
||||
logBuilder.WriteString(fmt.Sprintf("客户端IP: %s\n", r.RemoteAddr))
|
||||
|
||||
// 记录请求头
|
||||
logBuilder.WriteString("--- 请求头 ---\n")
|
||||
for name, values := range r.Header {
|
||||
for _, value := range values {
|
||||
logBuilder.WriteString(fmt.Sprintf("%s: %s\n", name, value))
|
||||
}
|
||||
}
|
||||
|
||||
// 记录请求体
|
||||
logBuilder.WriteString("--- 请求体 ---\n")
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
logBuilder.WriteString(fmt.Sprintf("读取请求体失败: %v\n", err))
|
||||
} else {
|
||||
if len(body) > 0 {
|
||||
logBuilder.WriteString(fmt.Sprintf("%s\n", string(body)))
|
||||
} else {
|
||||
logBuilder.WriteString("(空请求体)\n")
|
||||
}
|
||||
}
|
||||
|
||||
// 处理XML请求体和第三方推送
|
||||
var pushResponse string
|
||||
|
||||
log.Printf("body:%s\r\n", string(body))
|
||||
|
||||
if len(body) > 0 {
|
||||
// 尝试解析XML
|
||||
var contractRoot ContractRoot
|
||||
if err := xml.Unmarshal(body, &contractRoot); err == nil {
|
||||
logBuilder.WriteString("--- XML解析结果 ---\n")
|
||||
logBuilder.WriteString(fmt.Sprintf("TYPE: %s\n", contractRoot.Type))
|
||||
logBuilder.WriteString(fmt.Sprintf("ICCID: %s\n", contractRoot.ICCID))
|
||||
logBuilder.WriteString(fmt.Sprintf("STATUSINFO: %s\n", contractRoot.StatusInfo))
|
||||
|
||||
// 如果TYPE=1,表示实名认证成功,需要推送到第三方
|
||||
if strings.Contains(contractRoot.AcceptMsg, "已完成实名信息补录") && contractRoot.ICCID != "" && contractRoot.ResultMsg == "成功" {
|
||||
logBuilder.WriteString("--- 第三方推送 ---\n")
|
||||
logBuilder.WriteString(fmt.Sprintf("触发条件: TYPE=%s (实名认证成功)\n", contractRoot.Type))
|
||||
logBuilder.WriteString(fmt.Sprintf("推送ICCID: %s\n", contractRoot.ICCID))
|
||||
logBuilder.WriteString("推送地址: http://jh.whjhft.com/gswlpushapi/recv.do?type=3\n")
|
||||
|
||||
timestampStr := strconv.FormatInt(time.Now().Unix(), 10)
|
||||
ModifyDate(contractRoot.ICCID, timestampStr)
|
||||
|
||||
// 执行推送
|
||||
if resp, err := pushToThirdParty(contractRoot.ICCID); err != nil {
|
||||
logBuilder.WriteString(fmt.Sprintf("推送失败: %v\n", err))
|
||||
pushResponse = fmt.Sprintf("推送失败: %v", err)
|
||||
} else {
|
||||
logBuilder.WriteString(fmt.Sprintf("推送成功,响应: %s\n", resp))
|
||||
pushResponse = resp
|
||||
}
|
||||
} else if strings.Contains(contractRoot.AcceptMsg, "已完成实名信息清除") && contractRoot.ICCID != "" && contractRoot.ResultMsg == "成功" {
|
||||
logBuilder.WriteString("--- 第三方推送删除实名 ---\n")
|
||||
res := DelRealName(contractRoot.ICCID)
|
||||
logBuilder.WriteString(fmt.Sprintf("删除实名响应: %s\n", res))
|
||||
} else {
|
||||
logBuilder.WriteString("--- 第三方推送 ---\n")
|
||||
logBuilder.WriteString(fmt.Sprintf("跳过推送: TYPE=%s,%s,%s (非实名认证成功)\n", contractRoot.Type, contractRoot.AcceptMsg, contractRoot.ResultMsg))
|
||||
}
|
||||
} else {
|
||||
logBuilder.WriteString(fmt.Sprintf("XML解析失败: %v\n", err))
|
||||
}
|
||||
}
|
||||
|
||||
// 记录处理时间
|
||||
processTime := time.Since(startTime)
|
||||
logBuilder.WriteString(fmt.Sprintf("处理耗时: %v\n", processTime))
|
||||
logBuilder.WriteString("========================================\n")
|
||||
|
||||
// 写入日志文件
|
||||
writeLog(logBuilder.String())
|
||||
|
||||
// 同时输出到控制台
|
||||
fmt.Print(logBuilder.String())
|
||||
|
||||
// 返回成功响应
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
|
||||
// 构建响应数据
|
||||
responseData := map[string]interface{}{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"timestamp": timestamp,
|
||||
}
|
||||
|
||||
// 如果有推送响应,添加到响应中
|
||||
if pushResponse != "" {
|
||||
responseData["pushResponse"] = pushResponse
|
||||
}
|
||||
|
||||
respJSON, _ := json.Marshal(responseData)
|
||||
w.Write(respJSON)
|
||||
}
|
||||
|
||||
// 获取管理平台登录凭证
|
||||
func (ac Account) Login() (sessionid string) {
|
||||
var password string
|
||||
password = ac.Password
|
||||
var requestBody bytes.Buffer
|
||||
multipartWriter := multipart.NewWriter(&requestBody)
|
||||
multipartWriter.WriteField("username", ac.UserName)
|
||||
multipartWriter.WriteField("password", password)
|
||||
multipartWriter.Close()
|
||||
req, _ := http.NewRequest("POST", "http://jh.whjhft.com/pages/login.do", &requestBody)
|
||||
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36 Edg/123.0.0.0")
|
||||
req.Header.Set("Content-Type", multipartWriter.FormDataContentType())
|
||||
//client1 := http.DefaultClient
|
||||
client1 := &http.Client{
|
||||
CheckRedirect: func(req1 *http.Request, via []*http.Request) error {
|
||||
|
||||
strs := strings.Split(req1.URL.Path, ";")
|
||||
log.Printf("%s\r\n", req1.URL.Path)
|
||||
if len(strs) == 2 {
|
||||
strs1 := strings.Split(strs[1], "=")
|
||||
if len(strs1) == 2 {
|
||||
sessionid = strs1[1]
|
||||
}
|
||||
}
|
||||
// fmt.Printf("Redirect from '%s' to '%s'\n", via[0].URL, req1.URL.Path)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
resp, err := client1.Do(req)
|
||||
if err != nil {
|
||||
fmt.Println("Failed to send request:", err)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 处理响应
|
||||
_, err = ioutil.ReadAll(resp.Body)
|
||||
|
||||
if err != nil {
|
||||
fmt.Println("Failed to read response:", err)
|
||||
return
|
||||
}
|
||||
//fmt.Println("Response:", string(respBody))
|
||||
return
|
||||
}
|
||||
|
||||
// 移动实名回调
|
||||
func ChinaMobileCallback(w http.ResponseWriter, r *http.Request) {
|
||||
// 记录请求开始时间
|
||||
startTime := time.Now()
|
||||
timestamp := startTime.Format("2006-01-02 15:04:05")
|
||||
|
||||
// 构建日志内容
|
||||
var logBuilder strings.Builder
|
||||
logBuilder.WriteString("\n========================================\n")
|
||||
logBuilder.WriteString("【移动实名回调】\n")
|
||||
logBuilder.WriteString(fmt.Sprintf("请求时间: %s\n", timestamp))
|
||||
logBuilder.WriteString(fmt.Sprintf("请求方法: %s\n", r.Method))
|
||||
logBuilder.WriteString(fmt.Sprintf("完整URL: %s\n", r.URL.String()))
|
||||
logBuilder.WriteString(fmt.Sprintf("请求路径: %s\n", r.URL.Path))
|
||||
logBuilder.WriteString(fmt.Sprintf("查询参数: %s\n", r.URL.RawQuery))
|
||||
logBuilder.WriteString(fmt.Sprintf("客户端IP: %s\n", r.RemoteAddr))
|
||||
|
||||
// 记录请求头
|
||||
logBuilder.WriteString("--- 请求头 ---\n")
|
||||
for name, values := range r.Header {
|
||||
for _, value := range values {
|
||||
logBuilder.WriteString(fmt.Sprintf("%s: %s\n", name, value))
|
||||
}
|
||||
}
|
||||
|
||||
// 记录请求体
|
||||
logBuilder.WriteString("--- 请求体 ---\n")
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
logBuilder.WriteString(fmt.Sprintf("读取请求体失败: %v\n", err))
|
||||
} else {
|
||||
if len(body) > 0 {
|
||||
logBuilder.WriteString(fmt.Sprintf("%s\n", string(body)))
|
||||
var JSONData struct {
|
||||
Status string `json:"status"`
|
||||
Message string `json:"message"`
|
||||
Result []struct {
|
||||
RegStatus string `json:"regStatus"`
|
||||
BusiSeq string `json:"busiSeq"`
|
||||
Msisdn string `json:"msisdn"`
|
||||
Iccid string `json:"iccid"`
|
||||
} `json:"result"`
|
||||
}
|
||||
json.Unmarshal(body, &JSONData)
|
||||
if JSONData.Status == "0" && JSONData.Message == "正确" && len(JSONData.Result) > 0 && JSONData.Result[0].RegStatus == "00000" {
|
||||
iccid := JSONData.Result[0].Iccid
|
||||
if iccid == "" {
|
||||
msisdn := JSONData.Result[0].Msisdn //接入号
|
||||
account := Account{UserName: "18627991016", Password: "y123456"}
|
||||
sessionid := account.Login()
|
||||
iccid = getIccidByMsisdn(msisdn, sessionid)
|
||||
}
|
||||
if iccid == "" {
|
||||
return
|
||||
}
|
||||
|
||||
//推送修改过期时间
|
||||
timestampStr := strconv.FormatInt(time.Now().Unix(), 10)
|
||||
ModifyDate(iccid, timestampStr)
|
||||
|
||||
logBuilder.WriteString("--- 第三方推送 ---\n")
|
||||
logBuilder.WriteString(fmt.Sprintf("推送ICCID: %s\n", iccid))
|
||||
logBuilder.WriteString("推送地址: http://jh.whjhft.com/gswlpushapi/recv.do?type=3\n")
|
||||
|
||||
// 执行推送实名状态
|
||||
if resp, err := pushToThirdParty(iccid); err != nil {
|
||||
logBuilder.WriteString(fmt.Sprintf("推送失败: %v\n", err))
|
||||
} else {
|
||||
logBuilder.WriteString(fmt.Sprintf("推送成功,响应: %s\n", resp))
|
||||
}
|
||||
logBuilder.WriteString("--- 第三方推送 ---\n")
|
||||
|
||||
} else {
|
||||
logBuilder.WriteString("(实名认证失败)\n")
|
||||
}
|
||||
|
||||
} else {
|
||||
logBuilder.WriteString("(空请求体)\n")
|
||||
}
|
||||
}
|
||||
defer r.Body.Close()
|
||||
|
||||
// 记录处理时间
|
||||
processTime := time.Since(startTime)
|
||||
logBuilder.WriteString(fmt.Sprintf("处理耗时: %v\n", processTime))
|
||||
logBuilder.WriteString("========================================\n")
|
||||
|
||||
// 写入日志文件
|
||||
writeLog(logBuilder.String())
|
||||
|
||||
// 同时输出到控制台
|
||||
fmt.Print(logBuilder.String())
|
||||
|
||||
// 返回200 OK响应
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
|
||||
// 构建响应数据
|
||||
responseData := map[string]interface{}{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"timestamp": timestamp,
|
||||
}
|
||||
|
||||
respJSON, _ := json.Marshal(responseData)
|
||||
w.Write(respJSON)
|
||||
}
|
||||
|
||||
// 联通解除实名回调
|
||||
func UniRealnameRemove(w http.ResponseWriter, r *http.Request) {
|
||||
// 记录请求开始时间
|
||||
startTime := time.Now()
|
||||
timestamp := startTime.Format("2006-01-02 15:04:05")
|
||||
|
||||
// 构建日志内容
|
||||
var logBuilder strings.Builder
|
||||
logBuilder.WriteString("\n========================================\n")
|
||||
logBuilder.WriteString(fmt.Sprintf("请求时间: %s\n", timestamp))
|
||||
logBuilder.WriteString(fmt.Sprintf("请求方法: %s\n", r.Method))
|
||||
logBuilder.WriteString(fmt.Sprintf("完整URL: %s\n", r.URL.String()))
|
||||
logBuilder.WriteString(fmt.Sprintf("请求路径: %s\n", r.URL.Path))
|
||||
logBuilder.WriteString(fmt.Sprintf("查询参数: %s\n", r.URL.RawQuery))
|
||||
logBuilder.WriteString(fmt.Sprintf("客户端IP: %s\n", r.RemoteAddr))
|
||||
|
||||
// 记录请求头
|
||||
logBuilder.WriteString("--- 请求头 ---\n")
|
||||
for name, values := range r.Header {
|
||||
for _, value := range values {
|
||||
logBuilder.WriteString(fmt.Sprintf("%s: %s\n", name, value))
|
||||
}
|
||||
}
|
||||
|
||||
// 记录请求体
|
||||
logBuilder.WriteString("--- 请求体 ---\n")
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
logBuilder.WriteString(fmt.Sprintf("读取请求体失败: %v\n", err))
|
||||
} else {
|
||||
if len(body) > 0 {
|
||||
logBuilder.WriteString(fmt.Sprintf("%s\n", string(body)))
|
||||
} else {
|
||||
logBuilder.WriteString("(空请求体)\n")
|
||||
}
|
||||
}
|
||||
defer r.Body.Close()
|
||||
|
||||
// 解析回调数据
|
||||
iccid, dateChanged, err := parseCallback(body)
|
||||
if err != nil {
|
||||
logBuilder.WriteString(fmt.Sprintf("解析回调数据失败: %v\n", err))
|
||||
} else {
|
||||
if len(iccid) == 20 {
|
||||
//取前面19位
|
||||
iccid = iccid[:19]
|
||||
//删除实名
|
||||
res := DelRealName(iccid)
|
||||
logBuilder.WriteString(fmt.Sprintf("删除实名响应: %s\n", res))
|
||||
}
|
||||
logBuilder.WriteString(fmt.Sprintf("解析回调数据成功: ICCID=%s, DateChanged=%s\n", iccid, dateChanged))
|
||||
}
|
||||
|
||||
// 写入日志文件
|
||||
writeLog(logBuilder.String())
|
||||
|
||||
// 同时输出到控制台
|
||||
fmt.Print(logBuilder.String())
|
||||
|
||||
// 构建响应数据
|
||||
responseData := map[string]interface{}{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"timestamp": timestamp,
|
||||
}
|
||||
|
||||
respJSON, _ := json.Marshal(responseData)
|
||||
w.Write(respJSON)
|
||||
}
|
||||
|
||||
func main() {
|
||||
// 注册路由
|
||||
// res := DelRealName("8986112422108176397")
|
||||
// log.Printf("删除实名响应: %s", res)
|
||||
|
||||
http.HandleFunc("/5gcmp/callback/realname", handleCallback)
|
||||
http.HandleFunc("/unicom/callback/realname/remove", UniRealnameRemove)
|
||||
http.HandleFunc("/mobile/callback/realname", ChinaMobileCallback)
|
||||
|
||||
// 启动服务器
|
||||
port := ":16159"
|
||||
fmt.Printf("5GCMP回调服务器启动成功\n")
|
||||
fmt.Printf("监听端口: %s\n", port)
|
||||
fmt.Printf("电信回调地址: %s/5gcmp/callback/realname\n", port)
|
||||
fmt.Printf("联通回调地址: %s/unicom/callback/realname/remove\n", port)
|
||||
fmt.Printf("移动回调地址: %s/mobile/callback/realname\n", port)
|
||||
fmt.Printf("日志目录: logs/\n")
|
||||
fmt.Printf("按 Ctrl+C 停止服务器\n\n")
|
||||
|
||||
if err := http.ListenAndServe(port, nil); err != nil {
|
||||
log.Fatalf("启动服务器失败: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
type Account struct {
|
||||
UserName string
|
||||
Password string
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
优化轮询以及添加事件/触发式 数据同步
|
||||
|
||||
目前我们系统过于依赖轮询系统,且轮询系统的黑盒属性过于严重
|
||||
所以现在想加入事件触发以及实名回调,目前已知的可配置实名回调只有移动,联通,电信三个运营商,广电是没有实名回调的,上方是之前已经实现过的实名回调
|
||||
|
||||
需求差不多是这么个需求,主要就是想让数据同步这一块的及时率达到一种很快的地步,看是怎么埋点,而且开放接口的埋点还需要特殊处理,不然的话代理拿着我们的开放接口乱调用的话就等于外置了一个轮询系统了
|
||||
Reference in New Issue
Block a user