feat: 添加阿里云短信服务支持并优化验证码逻辑

- 引入 alibabacloud/dysmsapi-20170525 依赖,集成阿里云短信服务
- 将短信验证码从6位改为4位,优化用户体验
- 添加聚水潭商品按时间范围同步功能,支持分页拉取
- 增强Cron任务异常处理,添加错误日志记录
- 修复系统附件服务中图片路径处理逻辑
- 更新阿里云SDK相关组件至最新版本
- 添加think-trace调试扩展,便于问题排查
- 优化跨域中间件,使用str_contains替代strpos
This commit is contained in:
2026-04-30 15:19:40 +08:00
parent 0334fef6c1
commit c7f85ffbda
565 changed files with 59742 additions and 3391 deletions

View File

@@ -0,0 +1,5 @@
- [ ] `route/admin.php` 中正确添加了 `sync_jst_product_by_time` 路由。
- [ ] `StoreProduct::sync_jst_product_by_time` 能够正确接收并校验 `start_time` 和 `end_time` 参数。
- [ ] `HaichaoJstServices::getProductListByTime` 能够正确构建请求参数(包括 `modified_begin` 和 `modified_end`)并返回数据。
- [ ] `ProductSyncErp` 中的商品处理逻辑成功抽离为 `doProcessProductList`,且原有的按 SPU 同步功能未受影响。
- [ ] `ProductSyncErp::productFromErpByTime` 能够正确处理聚水潭的分页返回,直到所有页数处理完毕。

View File

@@ -0,0 +1,31 @@
# 增加聚水潭商品按时间范围同步功能 Spec
## Why
当前系统仅支持通过手动输入 SPU 编码列表的方式来同步聚水潭商品。在实际运营中,管理员往往需要批量同步某段时间内(如今天、本周)在聚水潭中新增或修改的商品。增加按时间范围同步的功能可以大大提升操作效率,避免遗漏。
## What Changes
- 在后台路由配置文件中新增 `sync_jst_product_by_time` 路由。
- 在 `StoreProduct` 控制器中新增 `sync_jst_product_by_time` 方法,用于接收 `start_time`、`end_time` 和 `sync` 参数,并分发同步任务。
- 在 `HaichaoJstServices` 服务类中新增 `getProductListByTime` 方法,封装调用聚水潭 `open/mall/item/query` 接口的时间范围查询逻辑。
- 在 `ProductSyncErp` 任务类中新增 `productFromErpByTime` 方法,支持按页拉取时间范围内的商品。
- **重构**: 将 `ProductSyncErp::productFromErp` 中核心的商品数据转换和保存逻辑抽离为 `doProcessProductList` 方法,以便按 SPU 同步和按时间同步复用相同的处理逻辑。
## Impact
- Affected specs: 聚水潭商品手动同步功能。
- Affected code:
- `route/admin.php`
- `app/controller/admin/v1/product/StoreProduct.php`
- `app/haichao/HaichaoJstServices.php`
- `app/jobs/product/ProductSyncErp.php`
## ADDED Requirements
### Requirement: 按时间范围同步商品
系统 SHALL 提供一个接口,允许管理员传入开始时间和结束时间,自动拉取聚水潭在该时间范围内修改过的商品,并同步到商城系统中。
#### Scenario: 成功发起异步同步任务
- **WHEN** 管理员调用 `/adminapi/product/sync_jst_product_by_time` 接口,传入有效的 `start_time` 和 `end_time`,且 `sync=0`
- **THEN** 系统返回成功响应,并在后台队列中创建对应的同步任务,按页拉取并更新商品数据。
## MODIFIED Requirements
### Requirement: 重构商品同步核心逻辑
为了避免代码重复,现有的按 SPU 同步商品的逻辑 SHALL 被重构。商品数据的映射、价格计算、SKU 组装和入库逻辑 SHALL 抽取为独立的内部方法,供所有同步入口调用。

View File

@@ -0,0 +1,13 @@
# Tasks
- [ ] Task 1: 增加路由和控制器方法
- [ ] SubTask 1.1: 在 `route/admin.php` 中增加 `POST sync_jst_product_by_time` 路由。
- [ ] SubTask 1.2: 在 `app/controller/admin/v1/product/StoreProduct.php` 中实现 `sync_jst_product_by_time` 方法,验证时间参数,并根据 `sync` 参数决定是同步执行还是投递异步队列。
- [ ] Task 2: 在服务类中增加 API 调用方法
- [ ] SubTask 2.1: 在 `app/haichao/HaichaoJstServices.php` 中新增 `getProductListByTime(string $startTime, string $endTime, int $pageIndex = 1, int $pageSize = 50)` 方法。
- [ ] Task 3: 重构同步任务类并实现时间范围同步
- [ ] SubTask 3.1: 在 `app/jobs/product/ProductSyncErp.php` 中,将 `productFromErp` 方法内的商品列表遍历处理逻辑提取为受保护的 `doProcessProductList(array $productList, int $supplierId)` 方法。
- [ ] SubTask 3.2: 修改原 `productFromErp` 方法以调用新抽离的 `doProcessProductList`。
- [ ] SubTask 3.3: 在 `ProductSyncErp.php` 中新增 `productFromErpByTime(string $startTime, string $endTime, int $supplierId = 0)` 方法,实现分页拉取逻辑,并在每页数据返回后调用 `doProcessProductList`。

View File

@@ -0,0 +1,7 @@
- [ ] 已列出回调入口路由与调用链路:`/api/work/serve` -> `Wechat::work()` -> `WechatServices::workServe()` -> `Work::serve()`
- [ ] 已列出 Work 基础配置项与系统配置键映射:`corp_id/token/aes_key` -> `wechat_work_corpid/wechat_work_token/wechat_work_aes_key`
- [ ] 已列出 Work 应用配置项(通讯录/客户联系/自建应用)与系统配置键:`wechat_work_address_secret`、`wechat_work_user_secret`、`wechat_work_build_agent_id`、`wechat_work_build_secret`
- [ ] 已说明 `Failed to decode request contents.` 的触发原因与正确校验方式(企业微信 GET 校验、POST 加密回调)
- [ ] 已给出“域名无法配置”的替代接入方案,并明确透明转发必须保持 query/body 不变
- [ ] 已给出“无法接入回调”的降级方案:使用后台同步成员能力,并标注其删除/全量重建的副作用
- [ ](可选增强)若实现 `wechat_work_callback_url`,后台表单能正确展示并可复制,空值时回退到 `site_url . '/api/work/serve'`

View File

@@ -0,0 +1,76 @@
# 企业微信通讯录配置项使用逻辑与回调域名替代方案 Spec
## Why
现网企业微信通讯录/客户联系能力依赖“接收事件服务器”回调地址(如 `https://shop.douba.top/api/work/serve`)来接收成员/部门/客户等变更事件。实际部署中可能遇到企业微信后台无法填写或无法通过校验(域名限制、HTTPS 证书、网络可达性、网关/WAF 等)导致无法上线。
本 Spec 的目标是:
- 梳理后端企业微信通讯录相关配置项在代码中的读取、组装与使用路径,便于定位配置错误。
- 给出当企业微信侧无法配置项目域名时的可行替代配置方式(运维/网络层方案 + 业务层降级方案)。
- (可选增强)提供独立的“企业微信回调地址”配置项,仅用于展示与复制,避免必须修改全站 `site_url`。
## Current Behavior(现状)
### 1) 回调路由入口
- 企业微信回调入口固定为 `/api/work/serve`,路由映射到 `app\controller\api\v1\wechat\Wechat::work()`,最终调用 `crmeb\services\wechat\Work::serve()`。
- 后台配置表单中“服务器地址”字段为只读展示:`sys_config('site_url') . '/api/work/serve'`。
### 2) 配置项来源与组装
企业微信配置通过两层组装后交给 EasyWeChat:
- **基础配置**(用于验签/解密/加密回复):`corp_id`、`token`、`aes_key`
- 映射到系统配置键:`wechat_work_corpid`、`wechat_work_token`、`wechat_work_aes_key`
- **应用配置**(用于调用不同能力 API):按类型注入 `secret` / `agent_id`
- 通讯录同步:`wechat_work_address_secret`
- 客户联系:`wechat_work_user_secret`
- 自建应用:`wechat_work_build_agent_id` + `wechat_work_build_secret`
### 3) 回调验签与事件处理
- `Work::serve()` 调用 EasyWeChat Work Server 完成 URL 校验与消息解密;解密后消息交给 `app\listener\wechat\WorkListener` 分发。
- `WorkListener` 处理的关键事件包含:
- 通讯录事件:`change_contact`(成员/部门增删改)
- 客户事件:`change_external_contact`
- 客户群事件:`change_external_chat`
- 客户标签事件:`change_external_tag`
## Problems(常见问题)
- 直接在浏览器访问回调 URL 或用普通 HTTP 客户端随意请求,会触发 EasyWeChat 的解析失败,出现 `Failed to decode request contents.`;这不代表系统异常,只是请求不是企业微信回调协议(缺少 `echostr/msg_signature` 或正文不是企业微信 XML/加密格式)。
- 企业微信后台“接收事件服务器”校验失败,常见原因:
- URL 不可公网访问(内网/防火墙/白名单/运营商限制)
- HTTPS 证书链不完整/过期/不被信任
- 反向代理/WAF 改写了 query/body(导致 `msg_signature` 校验失败)
- 回调域名无法在企业微信侧配置(企业侧安全策略或域名限制)
## What Changes(拟新增/调整)
### A. 文档化与定位指引(必做)
- 输出一份“配置项—代码使用点—故障排查清单”的说明(本 Spec + checklist)。
- 明确哪些配置影响“回调校验”,哪些配置只影响“API 调用”。
### B. 回调域名替代方案(不改代码即可落地)
- 允许使用“可在企业微信侧配置的域名/网关”作为回调入口,通过透明转发(反向代理/网关/隧道)转发到实际项目的 `/api/work/serve`。
- 允许在无法接入回调时,使用后台已有的“同步企业微信成员”能力做业务降级(非实时)。
### C. 可选增强:增加独立的回调地址配置项(需要改代码)
新增系统配置键(示例):`wechat_work_callback_url`
- 用途:仅用于后台表单展示/复制,默认值仍为 `site_url . '/api/work/serve'`。
- 不改变实际回调逻辑(仍然由路由 `/api/work/serve` 接收);目的是让运维能把“实际对外可用的回调域名”记录在系统里,减少误配。
## Requirements
### Requirement 1: 配置项可追溯
系统 SHALL 明确以下配置项的用途与作用范围:
- `wechat_work_corpid` / `wechat_work_token` / `wechat_work_aes_key`:影响回调验签解密与企业微信身份
- `wechat_work_address_secret`:影响通讯录同步相关 API(拉部门、拉成员等)
- `wechat_work_user_secret`:影响客户联系相关 API
- `wechat_work_build_agent_id` + `wechat_work_build_secret`:影响自建应用相关 API
### Requirement 2: 当域名无法配置时可替代接入
在企业微信后台无法配置项目域名时,运维 SHALL 能选择以下一种方式完成回调接入:
- 方案 A:配置一个企业微信可接受的域名作为回调入口,并反向代理透明转发到本项目 `/api/work/serve`
- 方案 B:无法接入回调时,使用后台“同步企业微信成员”接口进行人工/定时全量同步作为降级(承认非实时)
### Requirement 3(可选): 回调地址可配置展示
系统 SHOULD 提供 `wechat_work_callback_url` 配置项用于展示与复制,默认回退到 `site_url . '/api/work/serve'`。
## Impact(影响面)
### 不改代码即可采用
- 仅影响运维部署方式与企业微信后台填写的回调 URL。
### 若做可选增强
- 影响后台“企业微信配置”表单展示字段。

View File

@@ -0,0 +1,19 @@
# Tasks
- [ ] Task 1: 梳理配置项与使用链路(文档输出)
- [ ] SubTask 1.1: 汇总企业微信回调入口、路由、控制器与服务调用链路。
- [ ] SubTask 1.2: 汇总企业微信配置项(sys_config 键)与代码读取点、作用范围(回调验签 vs API 调用)。
- [ ] SubTask 1.3: 汇总回调事件类型与本地处理服务(通讯录/客户/客户群/标签)。
- [ ] Task 2: 给出“无法配置域名”的替代接入方式(运维/网络层)
- [ ] SubTask 2.1: 反向代理/网关透明转发方案(保持 query/body 不变,避免验签失败)。
- [ ] SubTask 2.2: 备用公网域名/子域名方案(同服务器多域名证书)。
- [ ] SubTask 2.3: 隧道映射方案(frp/ngrok 类,仅建议测试或临时)。
- [ ] Task 3: 给出“无法接入回调”的业务降级方式(不改代码)
- [ ] SubTask 3.1: 使用后台“同步企业微信成员”接口触发全量同步(说明副作用与适用场景)。
- [ ] SubTask 3.2: 建议定时执行策略(如低频全量同步,或按部门增量同步的可拓展方向)。
- [ ] Task 4(可选增强): 增加独立的回调地址展示配置项
- [ ] SubTask 4.1: 增加系统配置键 `wechat_work_callback_url`(若已有配置表/初始化机制则按既有方式接入)。
- [ ] SubTask 4.2: 修改企业微信配置表单展示字段,优先展示 `wechat_work_callback_url`,空则回退 `site_url . '/api/work/serve'`。

View File

@@ -0,0 +1,568 @@
# `/adminapi/product/sync_jst_product` 接口分析
## 一、接口概览
| 属性 | 值 |
|------|-----|
| **接口地址** | `/adminapi/product/sync_jst_product` |
| **请求方法** | `POST` |
| **路由定义** | [admin.php#L1627](file:///home/jone/workspace/CRMEB_PRO/route/admin.php#L1627) |
| **控制器方法** | [StoreProduct.php#L997-1028](file:///home/jone/workspace/CRMEB_PRO/app/controller/admin/v1/product/StoreProduct.php#L997-1028) |
| **核心业务逻辑** | [ProductSyncErp.php#L122-320](file:///home/jone/workspace/CRMEB_PRO/app/jobs/product/ProductSyncErp.php#L122-320) |
| **功能说明** | 从聚水潭ERP手动同步商品到CRMEB平台 |
---
## 二、接口请求
### 2.1 请求参数
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|--------|------|------|--------|------|
| `spu_str` | string | ✅ 是 | "" | ERP商品SPU,多个用逗号分隔 |
| `sync` | int | ❌ 否 | 0 | 同步方式:0=异步队列,1=同步执行 |
### 2.2 请求示例
```bash
# 异步同步(默认)
POST /adminapi/product/sync_jst_product
Content-Type: application/json
{
"spu_str": "4651127369,5606676507",
"sync": 0
}
# 同步执行
POST /adminapi/product/sync_jst_product
Content-Type: application/json
{
"spu_str": "4651127369",
"sync": 1
}
```
---
## 三、业务流程
### 3.1 整体流程图
```
┌─────────────────────────────────────────────┐
│ POST /adminapi/product/sync_jst_product │
└─────────────────┬───────────────────────────┘
│
▼
┌─────────────────┐
│ 参数验证 │
│ spu_str 不能为空│
└────────┬────────┘
│
▼
┌─────────────────┐
│ 解析SPU数组 │
│ 去重 + 过滤空值 │
└────────┬────────┘
│
▼
┌─────────────────┐
│ 生成TraceID │
│ jst_xxxxxxxxx │
└────────┬────────┘
│
▼
┌─────────────────────────────────┐
│ 记录操作日志 │
│ admin_id, supplier_id, count │
└────────┬────────────────────────┘
│
┌────────┴────────┐
│ sync 参数判断 │
└────────┬────────┘
│
┌────────┴────────┐
│ sync = 1? │
└────────┬────────┘
是 │ 否
┌──────┴──────┐
▼ ▼
┌────────────┐ ┌─────────────┐
│ 同步执行 │ │ 异步队列执行 │
│ productFrom│ │ ProductSync │
│ Erp() │ │ Erp:: │
│ │ │ dispatchDo()│
└──────┬─────┘ └──────┬──────┘
│ │
└───────┬───────┘
▼
┌─────────────────┐
│ 返回响应 │
└─────────────────┘
```
---
## 四、核心业务逻辑
### 4.1 控制器层(StoreProduct::sync_jst_product)
**文件**: [StoreProduct.php#L997-1028](file:///home/jone/workspace/CRMEB_PRO/app/controller/admin/v1/product/StoreProduct.php#L997-1028)
```php
public function sync_jst_product()
{
// 1. 获取请求参数
[$spuStr, $sync] = $this->request->postMore([
['spu_str', ""], // SPU字符串
['sync', 0] // 同步方式
], true);
// 2. 参数验证
if (!$spuStr) {
return $this->fail('请输入ERP商品SPU');
}
// 3. 解析SPU数组(去重+过滤空值)
$spus = array_unique(array_filter(explode(',', $spuStr)));
// 4. 生成追踪ID
$traceId = uniqid('jst_', true);
// 5. 记录操作日志
Log::info('admin.manual.jst.product.sync.dispatch', [
'trace_id' => $traceId,
'admin_id' => $this->adminId,
'admin_type' => $this->adminType,
'supplier_id' => $this->supplierId,
'client_ip' => $this->request->ip(),
'count' => count($spus),
'spus' => $spus
]);
// 6. 根据sync参数选择执行方式
if ($sync) {
// 同步执行
foreach ($spus as $spu) {
(new ProductSyncErp())->productFromErp($spu, (int)$this->supplierId);
}
return $this->success('同步完成');
} else {
// 异步队列执行
foreach ($spus as $spu) {
ProductSyncErp::dispatchDo('productFromErp', [$spu, (int)$this->supplierId]);
}
return $this->success('正在同步中,请稍后查看');
}
}
```
**关键特性**:
- ✅ 支持批量SPU同步
- ✅ 支持同步/异步两种模式
- ✅ 完整的操作日志记录
- ✅ 支持供应商商品同步
---
### 4.2 服务层(ProductSyncErp::productFromErp)
**文件**: [ProductSyncErp.php#L122-320](file:///home/jone/workspace/CRMEB_PRO/app/jobs/product/ProductSyncErp.php#L122-320)
#### 步骤1: 调用聚水潭API获取商品数据
```php
// 调用HaichaoJstServices获取商品详情
$result = app()->make(HaichaoJstServices::class)->getProductListKuan($spuArr);
// API调用日志
Log::info('admin.manual.jst.product.sync.api_result', [
'spu' => $spuArr,
'page_index' => $result['page_index'] ?? null,
'page_count' => $result['page_count'] ?? null,
'datas_count' => count($result['datas'] ?? []),
'result_preview' => json_encode($result),
]);
// 获取商品列表
$productList = $result['datas'] ?? [];
```
**调用API**: `/open/mall/item/query`
```php
$data = $this->post('open/mall/item/query', [
'i_ids' => [$key], // SPU ID数组
'page_index' => 1,
'page_size' => 10,
'date_field' => '',
])['data'];
```
#### 步骤2: 数据转换
遍历商品列表,将聚水潭数据转换为CRMEB格式:
```php
foreach ($productList as $item) {
// 1. 价格计算逻辑
$salePrice = floatval($item['s_price'] ?? 0); // SPU售价
$marketPrice = floatval($item['market_price'] ?? 0); // 市场价
$costPrice = floatval($item['c_price'] ?? 0); // 成本价
// 如果SPU价格为空,从SKU中获取
if ($salePrice <= 0 && $skuRows) {
$salePrice = min(array_column($skuRows, 'sale_price'));
}
// 2. 构建商品基础信息
$productInfo = [
'image' => $pic, // 商品主图
'slider_image' => json_encode([$pic]), // 轮播图
'store_name' => $name, // 商品名称
'store_info' => $name, // 商品简介
'cate_id' => 0, // 分类ID(待设置)
'price' => $salePrice, // 售价
'ot_price' => $marketPrice, // 划线价
'delivery_type' => '1,2,3', // 配送方式
'freight' => 1, // 运费模板
'is_show' => 0, // 默认下架状态
'add_time' => time(), // 添加时间
'cost' => $costPrice, // 成本价
'ficti' => 0, // 虚拟销量
'spec_type' => 1, // 规格类型
'code' => $item['i_id'], // ERP商品编码
'type' => $relationSupplierId ? 2 : 0, // 0=平台商品, 2=供应商商品
'relation_id' => $relationSupplierId, // 供应商ID
];
// 3. 构建SKU规格数据
foreach ($skuRows as $skuIndex => $items) {
$specValue = trim($items['properties_value'] ?? '');
if ($specValue === '') {
$specValue = trim($items['name'] ?? '');
}
if ($specValue === '') {
$specValue = '规格' . ($skuIndex + 1);
}
$value[] = [
'code' => $items['sku_id'], // SKU编码
'price' => floatval($items['sale_price'] ?? $salePrice), // 售价
'ot_price' => floatval($items['market_price'] ?? $marketPrice), // 划线价
'cost' => floatval($items['cost_price'] ?? $costPrice), // 成本价
'pic' => $items['pic'], // SKU图片
'detail' => ['规格' => $specValue], // 规格详情
'value1' => $specValue,
'values' => $specValue,
'stock' => 0, // 库存(待同步)
];
}
}
```
#### 步骤3: 判断新增或更新
```php
// 检查商品是否已存在
$pid = $productServices->value(['code' => $item['i_id']], 'id');
$existed = !!$pid;
if (!$pid) {
// 新增商品
$pid = $productServices->ErpProductSave($productInfo);
}
// 保存SKU规格
$skuList = $productAttrServices->validateProductAttr($attr, $value, $pid, 0, 0, 0);
$productAttrServices->saveProductAttr($skuList, $pid);
```
#### 步骤4: 清除缓存
```php
// 清除商品缓存
$productServices->cacheTag()->clear();
$productAttrServices->cacheTag()->clear();
```
---
## 五、数据映射关系
### 5.1 商品主数据
| CRMEB字段 | 聚水潭字段 | 说明 |
|-----------|-----------|------|
| `code` | `i_id` | SPU编码(唯一标识)|
| `store_name` | `name` | 商品名称 |
| `price` | `s_price` 或 SKU的最低售价 | 售价 |
| `ot_price` | `market_price` 或 SKU的最高市场价 | 划线价 |
| `cost` | `c_price` 或 SKU的最高成本价 | 成本价 |
| `image` | `pic` 或 SKU[0].pic | 商品主图 |
| `slider_image` | `pic` 的JSON数组 | 轮播图 |
| `type` | `relationSupplierId > 0 ? 2 : 0` | 0=平台商品, 2=供应商商品 |
### 5.2 SKU规格数据
| CRMEB字段 | 聚水潭字段 | 说明 |
|-----------|-----------|------|
| `code` | `sku_id` | SKU编码 |
| `price` | `sale_price` | SKU售价 |
| `ot_price` | `market_price` | SKU市场价 |
| `cost` | `cost_price` | SKU成本价 |
| `pic` | `pic` | SKU图片 |
| `detail` | `properties_value` | 规格属性 |
| `stock` | 0 | 库存(待后续同步)|
---
## 六、价格计算逻辑
### 6.1 SPU售价计算
```
1. 优先使用SPU的 s_price
2. 如果 s_price ≤ 0,则从SKU列表中取最低的 sale_price
```
### 6.2 市场价计算
```
1. 优先使用SPU的 market_price
2. 如果 market_price ≤ 0,则从SKU列表中取最高的 market_price
```
### 6.3 成本价计算
```
1. 优先使用SPU的 c_price
2. 如果 c_price ≤ 0,则从SKU列表中取最高的 cost_price
```
---
## 七、执行模式对比
### 7.1 同步执行(sync=1)
```php
foreach ($spus as $spu) {
(new ProductSyncErp())->productFromErp($spu, (int)$this->supplierId);
}
return $this->success('同步完成');
```
**特点**:
- ✅ 实时返回结果
- ✅ 立即看到同步状态
- ⚠️ 请求超时风险(大量SKU时)
- ⚠️ 用户需要等待
**适用场景**:
- 单个SPU同步
- 小批量同步(<10个)
- 开发测试
### 7.2 异步队列执行(sync=0)
```php
foreach ($spus as $spu) {
ProductSyncErp::dispatchDo('productFromErp', [$spu, (int)$this->supplierId]);
}
return $this->success('正在同步中,请稍后查看');
```
**特点**:
- ✅ 快速响应
- ✅ 支持大批量同步
- ✅ 不阻塞用户操作
- ✅ 队列重试机制
**适用场景**:
- 批量SPU同步
- 大批量同步(>10个)
- 生产环境推荐
---
## 八、日志追踪
### 8.1 操作日志节点
| 节点 | 日志Key | 记录内容 |
|------|---------|---------|
| 控制器入口 | `admin.manual.jst.product.sync.dispatch` | trace_id, admin_id, supplier_id, spu数组 |
| API调用 | `admin.manual.jst.product.sync.api_result` | API返回结果,数据量 |
| 遍历商品 | `admin.manual.jst.product.sync.item` | 每个商品的i_id, name, sku数量 |
| 保存商品 | `admin.manual.jst.product.sync.save` | product_id, existed, sku数量 |
| 完成 | `admin.manual.jst.product.sync.finish` | 完成数量 |
| 错误 | `admin.manual.jst.product.sync.error` | 错误信息 |
### 8.2 日志示例
```json
{
"log_type": "admin.manual.jst.product.sync.dispatch",
"trace_id": "jst_647a8b9c12345",
"admin_id": 1,
"supplier_id": 0,
"count": 2,
"spus": ["4651127369", "5606676507"]
}
```
---
## 九、错误处理
### 9.1 异常捕获
```php
try {
// 业务逻辑
} catch (\Exception $e) {
// 记录错误日志
response_log_write([
'message' => '商品同步失败, 原因: ' . $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine()
]);
Log::error('admin.manual.jst.product.sync.error', [
'spu' => $spuArr,
'message' => $e->getMessage()
]);
}
```
### 9.2 常见错误
| 错误类型 | 原因 | 处理方式 |
|---------|------|---------|
| Token无效 | JST_ACCESS_TOKEN过期 | 重新获取Token |
| SPU不存在 | 聚水潭中无此商品 | 检查SPU编码 |
| 网络超时 | API调用超时 | 队列重试 |
| 数据库错误 | 保存商品失败 | 检查数据库连接 |
---
## 十、性能优化建议
### 10.1 当前实现
- ✅ 支持批量SPU同步
- ✅ 异步队列处理
- ✅ 完整的日志追踪
- ✅ 商品/SKU数据缓存
### 10.2 优化建议
#### 建议1: 批量API调用
**当前**: 每个SPU调用一次API
**优化**:
```php
// 批量查询(需要聚水潭API支持)
$data = $this->post('open/mall/item/query', [
'i_ids' => $spuArr, // 一次性传入多个SPU
]);
```
#### 建议2: 批量保存
**当前**: 逐个保存商品
**优化**:
```php
// 批量插入
$productServices->saveAll($productList);
$productAttrServices->saveAll($skuList);
```
#### 建议3: 库存同步
**当前**: 库存默认为0
**优化**:
```php
// 批量查询库存
$skuIds = array_column($skuRows, 'sku_id');
$stockMap = $this->getProductListSku(implode(',', $skuIds));
// 更新库存
foreach ($skuList as &$sku) {
$sku['stock'] = $stockMap[$sku['code']]['qty'] ?? 0;
}
```
---
## 十一、使用示例
### 11.1 单个SPU同步
```bash
# 异步同步
curl -X POST /adminapi/product/sync_jst_product \
-H "Content-Type: application/json" \
-d '{"spu_str": "4651127369"}'
# 响应: {"status": 200, "msg": "正在同步中,请稍后查看"}
```
### 11.2 批量SPU同步
```bash
# 异步同步多个SPU
curl -X POST /adminapi/product/sync_jst_product \
-H "Content-Type: application/json" \
-d '{"spu_str": "4651127369,5606676507,5633479406"}'
# 响应: {"status": 200, "msg": "正在同步中,请稍后查看"}
```
### 11.3 同步执行
```bash
# 同步执行(立即返回结果)
curl -X POST /adminapi/product/sync_jst_product \
-H "Content-Type: application/json" \
-d '{"spu_str": "4651127369", "sync": 1}'
# 响应: {"status": 200, "msg": "同步完成"}
```
---
## 十二、总结
### 核心流程
```
用户请求 → 参数验证 → 解析SPU →
↓
调用聚水潭API → 转换数据格式 →
↓
判断新增/更新 → 保存商品和SKU →
↓
清除缓存 → 返回结果
```
### 关键特性
1. **灵活的执行模式**: 支持同步/异步两种方式
2. **完整的数据映射**: 聚水潭 → CRMEB 字段映射完整
3. **智能价格计算**: 自动从SKU中获取最优价格
4. **完善的日志追踪**: 全链路日志记录
5. **供应商支持**: 支持平台商品和供应商商品
6. **错误处理**: 完整的异常捕获和日志记录
### 优化方向
1. 批量API调用(减少网络请求)
2. 批量数据库操作(提升性能)
3. 库存同步(补充库存数据)
4. 进度查询(实时查看同步状态)

View File

@@ -0,0 +1,54 @@
<?php
namespace app\command;
use app\services\product\product\StoreProductServices;
use think\console\Command;
use think\console\Input;
use think\console\Output;
use think\console\input\Option;
use think\facade\Db;
class BackfillProductSpu extends Command
{
protected function configure()
{
$this->setName('product:spu:backfill')
->addOption('chunk', null, Option::VALUE_REQUIRED, '单次处理条数', 200)
->addOption('limit', null, Option::VALUE_REQUIRED, '最多处理条数(0不限制)', 0)
->setDescription('Backfill empty product spu');
}
protected function execute(Input $input, Output $output)
{
$chunk = max(1, (int)$input->getOption('chunk'));
$limit = max(0, (int)$input->getOption('limit'));
$handled = 0;
/** @var StoreProductServices $productServices */
$productServices = app()->make(StoreProductServices::class);
while (true) {
$query = Db::name('store_product')->where('spu', '')->where('is_del', 0)->order('id', 'asc')->limit($chunk);
if ($limit > 0) {
$query->limit(min($chunk, $limit - $handled));
}
$rows = $query->field(['id'])->select()->toArray();
if (!$rows) {
break;
}
foreach ($rows as $row) {
$productServices->ensureSpu((int)$row['id']);
$handled++;
if ($limit > 0 && $handled >= $limit) {
break 2;
}
}
$output->info('handled: ' . $handled);
}
$output->info('done: ' . $handled);
return 0;
}
}

View File

@@ -0,0 +1,93 @@
<?php
namespace app\command;
use app\haichao\HaichaoJstServices;
use think\console\Command;
use think\console\Input;
use think\console\input\Argument;
use think\console\input\Option;
use think\console\Output;
class TestJstSpu extends Command
{
protected function configure()
{
$this->setName('test_jst_spu')
->addArgument('spu', Argument::REQUIRED, 'SPU编码')
->setDescription('测试聚水潭按SPU拉取商品接口');
}
protected function execute(Input $input, Output $output)
{
$spu = $input->getArgument('spu');
if (empty($spu)) {
$output->error('请提供SPU编码');
$output->info('用法: php think test_jst_spu SPU编码');
$output->info('示例: php think test_jst_spu test123');
return;
}
$output->info('正在调用聚水潭API获取商品信息...');
$output->info('SPU: ' . $spu);
$output->newLine();
try {
$startTime = microtime(true);
$haichaoJst = HaichaoJstServices::getServices();
$result = $haichaoJst->getProductListKuan($spu);
$endTime = microtime(true);
$costTime = round(($endTime - $startTime) * 1000, 2);
$output->info('✅ API调用成功! 耗时: ' . $costTime . 'ms');
$output->newLine();
if (empty($result)) {
$output->warning('⚠️ 未获取到任何数据');
return;
}
$output->info('📦 获取到的数据:');
$output->write(json_encode($result, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT));
$output->newLine();
if (isset($result['code'])) {
$output->info('响应码: ' . $result['code']);
}
if (isset($result['msg'])) {
$output->info('响应消息: ' . $result['msg']);
}
if (isset($result['datas'])) {
$output->info('商品数量: ' . count($result['datas']));
if (!empty($result['datas'])) {
$output->newLine();
$output->info('📋 第一个商品信息:');
$firstProduct = $result['datas'][0];
$output->writeln(' - SPU ID: ' . ($firstProduct['i_id'] ?? 'N/A'));
$output->writeln(' - 商品名称: ' . ($firstProduct['name'] ?? 'N/A'));
$output->writeln(' - 售价: ' . ($firstProduct['s_price'] ?? 'N/A'));
$output->writeln(' - 市场价: ' . ($firstProduct['market_price'] ?? 'N/A'));
$output->writeln(' - 成本价: ' . ($firstProduct['c_price'] ?? 'N/A'));
if (isset($firstProduct['skus']) && is_array($firstProduct['skus'])) {
$output->writeln(' - SKU数量: ' . count($firstProduct['skus']));
}
}
}
$output->newLine();
} catch (\Throwable $e) {
$output->error('❌ API调用失败!');
$output->error('错误信息: ' . $e->getMessage());
$output->error('错误文件: ' . $e->getFile() . ':' . $e->getLine());
$output->newLine();
$output->info('堆栈跟踪:');
$output->write($e->getTraceAsString());
}
}
}

View File

@@ -215,7 +215,7 @@ class Login
$verifyCode = CacheService::get('code_' . $phone);
if (!$verifyCode)
return app('json')->fail('请先获取验证码');
$verifyCode = substr($verifyCode, 0, 6);
$verifyCode = substr($verifyCode, 0, 4);
if ($verifyCode != $code) {
CacheService::delete('code_' . $phone);
return app('json')->fail('验证码错误');
@@ -262,7 +262,7 @@ class Login
$verifyCode = CacheService::get('code_' . $phone);
if (!$verifyCode)
return app('json')->fail('请先获取验证码');
$verifyCode = substr($verifyCode, 0, 6);
$verifyCode = substr($verifyCode, 0, 4);
if ($verifyCode != $code) {
CacheService::delete('code_' . $phone);
return app('json')->fail('验证码错误');

View File

@@ -996,37 +996,163 @@ class StoreProduct extends AuthController
public function sync_jst_product()
{
[$spuStr, $sync] = $this->request->postMore([
[$spuStr, $sync, $startTime, $endTime] = $this->request->postMore([
['spu_str', ""],
['sync', 0]
['sync', 0],
['start_time', ""],
['end_time', ""]
], true);
$traceId = uniqid('jst_sync_', true);
Log::info('admin.manual.jst.product.sync.request', [
'trace_id' => $traceId,
'admin_id' => $this->adminId,
'admin_type' => $this->adminType,
'supplier_id' => $this->supplierId,
'client_ip' => $this->request->ip(),
'sync' => (int)$sync,
'spu_str' => (string)$spuStr,
'start_time' => (string)$startTime,
'end_time' => (string)$endTime,
'start_time_is_iso' => is_string($startTime) && str_contains($startTime, 'T'),
'end_time_is_iso' => is_string($endTime) && str_contains($endTime, 'T'),
]);
if ($startTime && $endTime) {
$startTs = strtotime((string)$startTime);
$endTs = strtotime((string)$endTime);
if (!$startTs || !$endTs) {
Log::warning('admin.manual.jst.product.sync_by_time.invalid_time', [
'trace_id' => $traceId,
'start_time' => $startTime,
'end_time' => $endTime,
]);
return $this->fail('时间格式错误');
}
if ($endTs < $startTs) {
Log::warning('admin.manual.jst.product.sync_by_time.invalid_range', [
'trace_id' => $traceId,
'start_time' => $startTime,
'end_time' => $endTime,
]);
return $this->fail('时间范围错误');
}
if (($endTs - $startTs) > 7 * 86400) {
Log::info('admin.manual.jst.product.sync_by_time.range_too_large', [
'trace_id' => $traceId,
'start_time' => $startTime,
'end_time' => $endTime,
'range_seconds' => $endTs - $startTs,
]);
return $this->fail('时间范围不能超过7天');
}
$startTime = date('Y-m-d H:i:s', $startTs);
$endTime = date('Y-m-d H:i:s', $endTs);
if ($sync) {
Log::info('admin.manual.jst.product.sync_by_time.run', [
'trace_id' => $traceId,
'start_time' => $startTime,
'end_time' => $endTime,
'supplier_id' => $this->supplierId,
]);
$result = (new ProductSyncErp())->syncProductByTime($startTime, $endTime, (int)$this->supplierId, $traceId, 'sync');
if (!is_array($result) || empty($result['ok'])) {
return $this->fail('同步失败,请检查日志');
}
return $this->success('同步完成', $result);
} else {
Log::info('admin.manual.jst.product.sync_by_time.dispatch', [
'trace_id' => $traceId,
'start_time' => $startTime,
'end_time' => $endTime,
'supplier_id' => $this->supplierId,
]);
ProductSyncErp::dispatchDo('syncProductByTime', [$startTime, $endTime, (int)$this->supplierId, $traceId]);
return $this->success('正在按时间范围同步中,请稍后查看', ['trace_id' => $traceId]);
}
}
if (!$spuStr) {
return $this->fail('请输入ERP商品SPU');
return $this->fail('请输入ERP商品SPU或选择时间范围');
}
$spus = array_unique(array_filter(explode(',', $spuStr)));
$traceId = uniqid('jst_', true);
$spusLog = $spus;
$spusTotal = count($spus);
if ($spusTotal > 50) {
$spusLog = array_slice($spus, 0, 50);
}
Log::info('admin.manual.jst.product.sync.dispatch', [
'trace_id' => $traceId,
'admin_id' => $this->adminId,
'admin_type' => $this->adminType,
'supplier_id' => $this->supplierId,
'client_ip' => $this->request->ip(),
'count' => count($spus),
'spus' => $spus
'count' => $spusTotal,
'spus' => $spusLog,
'spus_truncated' => $spusTotal > 50
]);
if ($sync) {
$stats = [
'ok' => true,
'mode' => 'spu',
'trace_id' => $traceId,
'spu_count' => $spusTotal,
'fetched' => 0,
'created' => 0,
'updated' => 0,
'skipped' => 0,
'failed' => 0,
];
foreach ($spus as $spu) {
(new ProductSyncErp())->productFromErp($spu, (int)$this->supplierId);
$rt = (new ProductSyncErp())->productFromErp($spu, (int)$this->supplierId, $traceId);
if (is_array($rt)) {
foreach (['fetched', 'created', 'updated', 'skipped', 'failed'] as $k) {
$stats[$k] += (int)($rt[$k] ?? 0);
}
return $this->success('同步完成');
}
}
return $this->success('同步完成', $stats);
} else {
foreach ($spus as $spu) {
ProductSyncErp::dispatchDo('productFromErp', [$spu, (int)$this->supplierId]);
ProductSyncErp::dispatchDo('productFromErp', [$spu, (int)$this->supplierId, $traceId]);
}
return $this->success('正在同步中,请稍后查看');
return $this->success('正在同步中,请稍后查看', ['trace_id' => $traceId, 'task_count' => $spusTotal]);
}
}
public function upload_jst_itemsku()
{
[$ids] = $this->request->postMore([
['ids', []],
], true);
if (is_string($ids)) {
$ids = array_filter(explode(',', $ids));
}
$ids = is_array($ids) ? array_values(array_unique(array_map('intval', $ids))) : [];
if (!$ids) {
return $this->fail('请选择商品');
}
$traceId = uniqid('jst_itemsku_', true);
Log::info('admin.manual.jst.itemsku.upload.start', [
'trace_id' => $traceId,
'admin_id' => $this->adminId,
'admin_type' => $this->adminType,
'supplier_id' => $this->supplierId,
'client_ip' => $this->request->ip(),
'product_count' => count($ids),
'product_ids' => $ids,
]);
$rt = app()->make(\app\haichao\HaichaoJstServices::class)->uploadItemSkuByProductIds($ids);
Log::info('admin.manual.jst.itemsku.upload.end', [
'trace_id' => $traceId,
'upload_count' => $rt['upload_count'] ?? null,
]);
return $this->success($rt);
}
/**
* 商品批量操作
* @param StoreProductBatchProcessServices $batchProcessServices

View File

@@ -14,6 +14,7 @@ namespace app\controller\admin\v1\statistic;
use app\controller\admin\AuthController;
use app\services\statistic\OrderStatisticServices;
use think\annotation\Inject;
use think\facade\Log;
class OrderStatistic extends AuthController
{
@@ -36,6 +37,10 @@ class OrderStatistic extends AuthController
if($this->supplierId > 0){
$where['supplier_id'] = $this->supplierId;
}
$params = $this->request->param();
$paramsJson = json_encode($params, JSON_UNESCAPED_UNICODE);
$whereJson = json_encode($where, JSON_UNESCAPED_UNICODE);
Log::info('adminapi.statistic.order.get_basic supplierId:' . (int)$this->supplierId . ' params:' . ($paramsJson === false ? '' : $paramsJson) . ' where:' . ($whereJson === false ? '' : $whereJson));
$data = $this->services->getBasic($where);
return app('json')->success($data);
}

View File

@@ -86,7 +86,7 @@ class Login
$verifyCode = CacheService::get('code_' . $phone);
if (!$verifyCode)
return app('json')->fail('请先获取验证码');
$verifyCode = substr($verifyCode, 0, 6);
$verifyCode = substr($verifyCode, 0, 4);
if ($verifyCode != $captcha) {
CacheService::delete('code_' . $phone);
return app('json')->fail('验证码错误');
@@ -288,7 +288,7 @@ class Login
$verifyCode = CacheService::get('code_' . $account);
if (!$verifyCode)
return app('json')->fail('请先获取验证码');
$verifyCode = substr($verifyCode, 0, 6);
$verifyCode = substr($verifyCode, 0, 4);
if ($verifyCode != $captcha)
return app('json')->fail('验证码错误');
if (strlen(trim($password)) < 6 || strlen(trim($password)) > 16)
@@ -318,7 +318,7 @@ class Login
$verifyCode = CacheService::get('code_' . $account);
if (!$verifyCode)
return app('json')->fail('请先获取验证码');
$verifyCode = substr($verifyCode, 0, 6);
$verifyCode = substr($verifyCode, 0, 4);
if ($verifyCode != $captcha) {
return app('json')->fail('验证码错误');
}
@@ -353,7 +353,7 @@ class Login
$verifyCode = CacheService::get('code_' . $phone);
if (!$verifyCode)
return app('json')->fail('请先获取验证码');
$verifyCode = substr($verifyCode, 0, 6);
$verifyCode = substr($verifyCode, 0, 4);
if ($verifyCode != $captcha) {
return app('json')->fail('验证码错误');
}
@@ -418,7 +418,7 @@ class Login
$verifyCode = CacheService::get('code_' . $phone);
if (!$verifyCode)
return app('json')->fail('请先获取验证码');
$verifyCode = substr($verifyCode, 0, 6);
$verifyCode = substr($verifyCode, 0, 4);
if ($verifyCode != $captcha) {
CacheService::delete('code_' . $phone);
return app('json')->fail('验证码错误');
@@ -458,7 +458,7 @@ class Login
$verifyCode = CacheService::get('code_' . $phone);
if (!$verifyCode)
return app('json')->fail('请先获取验证码');
$verifyCode = substr($verifyCode, 0, 6);
$verifyCode = substr($verifyCode, 0, 4);
if ($verifyCode != $captcha)
return app('json')->fail('验证码错误');
}
@@ -488,7 +488,7 @@ class Login
$verifyCode = CacheService::get('code_' . $phone);
if (!$verifyCode)
return app('json')->fail('请先获取验证码');
$verifyCode = substr($verifyCode, 0, 6);
$verifyCode = substr($verifyCode, 0, 4);
if ($verifyCode != $captcha)
return app('json')->fail('验证码错误');
$uid = (int)$request->uid();

View File

@@ -104,7 +104,7 @@ class UserApply
$verifyCode = CacheService::get('code_' . $data['phone']);
if (!$verifyCode)
return app('json')->fail('请先获取验证码');
$verifyCode = substr($verifyCode, 0, 6);
$verifyCode = substr($verifyCode, 0, 4);
if ($verifyCode != $captcha) {
return app('json')->fail('验证码错误');
}

View File

@@ -76,7 +76,7 @@ class Channel
$verifyCode = CacheService::get('code_' . $data['phone']);
if (!$verifyCode)
return app('json')->fail('请先获取验证码');
$verifyCode = substr($verifyCode, 0, 6);
$verifyCode = substr($verifyCode, 0, 4);
if ($verifyCode != $data['code']) {
return app('json')->fail('验证码错误');
}

View File

@@ -127,7 +127,7 @@ class Wechat
$verifyCode = CacheService::get('code_' . $phone);
if (!$verifyCode)
return app('json')->fail('请先获取验证码');
$verifyCode = substr($verifyCode, 0, 6);
$verifyCode = substr($verifyCode, 0, 4);
if ($verifyCode != $captcha) {
CacheService::delete('code_' . $phone);
return app('json')->fail('验证码错误');

View File

@@ -72,7 +72,7 @@ class Routine
$verifyCode = CacheService::get('code_' . $phone);
if (!$verifyCode)
return app('json')->fail('请先获取验证码');
$verifyCode = substr($verifyCode, 0, 6);
$verifyCode = substr($verifyCode, 0, 4);
if ($verifyCode != $captcha) {
return app('json')->fail('验证码错误');
}
@@ -170,7 +170,7 @@ class Routine
$verifyCode = CacheService::get('code_' . $phone);
if (!$verifyCode)
return app('json')->fail('请先获取验证码');
$verifyCode = substr($verifyCode, 0, 6);
$verifyCode = substr($verifyCode, 0, 4);
if ($verifyCode != $captcha) {
CacheService::delete('code_' . $phone);
return app('json')->fail('验证码错误');

View File

@@ -112,7 +112,7 @@ class Wechat
$verifyCode = CacheService::get('code_' . $phone);
if (!$verifyCode)
return app('json')->fail('请先获取验证码');
$verifyCode = substr($verifyCode, 0, 6);
$verifyCode = substr($verifyCode, 0, 4);
if ($verifyCode != $captcha) {
CacheService::delete('code_' . $phone);
return app('json')->fail('验证码错误');

View File

@@ -108,6 +108,9 @@ class Common extends AuthController
*/
public function homeStatics(SupplierOrderServices $orderServices)
{
if (!(int)$this->supplierId) {
return $this->fail('登录已失效');
}
[$time] = $this->request->getMore([
['data', '', '', 'time']
], true);
@@ -123,6 +126,9 @@ class Common extends AuthController
*/
public function orderChart(SupplierOrderServices $orderServices)
{
if (!(int)$this->supplierId) {
return $this->fail('登录已失效');
}
[$time] = $this->request->getMore([
['data', '', '', 'time']
], true);
@@ -138,6 +144,9 @@ class Common extends AuthController
*/
public function orderType(SupplierOrderServices $orderServices)
{
if (!(int)$this->supplierId) {
return $this->fail('登录已失效');
}
[$time] = $this->request->getMore([
['data', '', '', 'time']
], true);
@@ -153,6 +162,9 @@ class Common extends AuthController
*/
public function orderChannel(SupplierOrderServices $orderServices)
{
if (!(int)$this->supplierId) {
return $this->fail('登录已失效');
}
[$time] = $this->request->getMore([
['data', '', '', 'time']
], true);
@@ -167,6 +179,9 @@ class Common extends AuthController
*/
public function jnotice()
{
if (!(int)$this->supplierId) {
return $this->fail('登录已失效');
}
/** @var StoreOrderServices $orderServices */
$orderServices = app()->make(StoreOrderServices::class);
$orderNum = $orderServices->storeOrderCount((int)$this->supplierId, -1, 'supplier_id');

View File

@@ -168,7 +168,7 @@ class Login
$verifyCode = CacheService::get('code_' . $account);
if (!$verifyCode)
return app('json')->fail('请先获取验证码');
$verifyCode = substr($verifyCode, 0, 6);
$verifyCode = substr($verifyCode, 0, 4);
if ($verifyCode != $captcha) {
return app('json')->fail('验证码错误');
}

View File

@@ -15,12 +15,14 @@ abstract class HaichaoBridge
'shopId' => '',
];
private string $tokenFile = 'runtime/jstdata/jst_token_data.json';
public function __construct()
{
$this->api['appid'] = env('JST_APP_KEY', '');
$this->api['appSecret'] = env('JST_APP_SECRET', '');
$this->api['api'] = env('JST_API', 'https://openapi.jushuitan.com/');
$this->api['access_token'] = env('JST_ACCESS_TOKEN', '');
$this->api['access_token'] = $this->getToken();
$this->api['shopId'] = (string)env('JST_SHOP_ID', '');
}
@@ -37,10 +39,38 @@ abstract class HaichaoBridge
* @return array
*/
protected function post(string $api, array $data = [], array $addonBody = []): array
{
return $this->postInternal($api, $data, $addonBody);
}
private function postInternal(string $api, array $data = [], array $addonBody = [], int $attempt = 0, bool $tokenRefreshed = false): array
{
if (!$this->api['appid'] || !$this->api['appSecret'] || !$this->api['access_token']) {
throw new ValidateException('JST config missing');
}
$apiName = ltrim($api, '/');
if ($apiName === 'open/inventory/query') {
$skuIdsRaw = $data['sku_ids'] ?? '';
$skuIdsList = [];
if (is_array($skuIdsRaw)) {
$skuIdsList = array_values(array_filter(array_map('trim', $skuIdsRaw), static fn($v) => $v !== ''));
$skuIdsRaw = implode(',', $skuIdsList);
} else {
$skuIdsRaw = (string)$skuIdsRaw;
$skuIdsList = array_values(array_filter(array_map('trim', explode(',', trim($skuIdsRaw, ", \t\n\r\0\x0B"))), static fn($v) => $v !== ''));
}
Log::info('jst.api.inventory.query.request ' . json_encode([
'api' => $apiName,
'attempt' => $attempt,
'page_index' => $data['page_index'] ?? null,
'page_num' => $data['page_num'] ?? null,
'page_size' => $data['page_size'] ?? null,
'sku_ids_count' => count($skuIdsList),
'sku_ids' => $skuIdsList,
'sku_ids_raw' => $skuIdsRaw,
], JSON_UNESCAPED_UNICODE));
}
$this->throttle();
$time = time();
$baseBody = [
'access_token' => $this->api['access_token'],
@@ -58,7 +88,19 @@ abstract class HaichaoBridge
'header' => 'Content-Type: application/json;charset=utf-8',
'content' => http_build_query($baseBody)
];
$rt = json_decode(file_get_contents("{$this->api['api']}$api", false, stream_context_create($options)), true);
$raw = file_get_contents("{$this->api['api']}$api", false, stream_context_create($options));
$rt = json_decode((string)$raw, true);
$respPreview = null;
try {
$respPreview = mb_substr((string)$raw, 0, 2000);
} catch (\Throwable $e) {
}
Log::info('jst.api.response ' . json_encode([
'api' => $api,
'code' => $rt['code'] ?? null,
'msg' => $rt['msg'] ?? null,
'preview' => $respPreview,
], JSON_UNESCAPED_UNICODE));
if (!is_array($rt) || !array_key_exists('code', $rt)) {
Log::error('JST response invalid ' . json_encode([
'api' => $api,
@@ -66,9 +108,19 @@ abstract class HaichaoBridge
throw new ValidateException('JST response invalid');
}
if ($rt['code'] != 0) {
$code = (int)$rt['code'];
if (!$tokenRefreshed && $this->isTokenError($rt)) {
$this->api['access_token'] = $this->getToken(true);
return $this->postInternal($api, $data, $addonBody, $attempt + 1, true);
}
if ($this->isRateLimitError($code) && $attempt < 6) {
$sleepMs = $this->getRateLimitSleepMs($code, $attempt);
usleep($sleepMs * 1000);
return $this->postInternal($api, $data, $addonBody, $attempt + 1, $tokenRefreshed);
}
Log::error('JST request failed ' . json_encode([
'api' => $api,
'code' => $rt['code'],
'code' => $code,
'msg' => (string)($rt['msg'] ?? ''),
], JSON_UNESCAPED_UNICODE));
throw new ValidateException($rt['msg'] ?? 'JST error');
@@ -76,6 +128,50 @@ abstract class HaichaoBridge
return $rt;
}
private function throttle(): void
{
static $lastAt = 0.0;
static $recent = [];
$minGap = 0.65;
while (true) {
$now = microtime(true);
$gap = $now - $lastAt;
if ($gap < $minGap) {
usleep((int)(($minGap - $gap) * 1000000));
continue;
}
$recent = array_values(array_filter($recent, static fn($t) => ($now - $t) < 60));
if (count($recent) >= 95) {
$oldest = $recent[0] ?? $now;
$wait = 60 - ($now - $oldest) + 0.2;
if ($wait > 0) {
usleep((int)($wait * 1000000));
continue;
}
}
$lastAt = microtime(true);
$recent[] = $lastAt;
return;
}
}
private function isRateLimitError(int $code): bool
{
return $code === 199 || $code === 200;
}
private function getRateLimitSleepMs(int $code, int $attempt): int
{
$jitter = random_int(0, 300);
if ($code === 200) {
return min(60000, (int)(6000 * (1 + $attempt)) + $jitter);
}
return min(10000, (int)(900 * (1 + $attempt)) + $jitter);
}
private function sign(array $data): string
{
$computerStr = "";
@@ -85,4 +181,167 @@ abstract class HaichaoBridge
$computerStr = $this->api['appSecret'] . $computerStr;
return md5($computerStr);
}
private function isTokenError(array $rt): bool
{
$msg = (string)($rt['msg'] ?? '');
if (!$msg) return false;
$msgLower = strtolower($msg);
return strpos($msgLower, 'access_token') !== false || strpos($msgLower, 'token') !== false;
}
private function getToken(bool $forceRefresh = false): string
{
$tokenInfo = $this->readTokenInfo();
if ($tokenInfo === null) {
$tokenInfo = $this->autoInitTokenFile();
if ($tokenInfo === null) {
throw new ValidateException('JST token missing, please check .env JST_ACCESS_TOKEN config');
}
}
if (!$forceRefresh && $tokenInfo) {
$accessToken = (string)($tokenInfo['access_token'] ?? '');
$expiresIn = (int)($tokenInfo['expires_in'] ?? 0);
$tokenTime = (int)($tokenInfo['time'] ?? 0);
if ($accessToken && $expiresIn > 0 && $tokenTime > 0) {
if (time() < ($tokenTime + $expiresIn - 3600)) {
return $accessToken;
}
}
}
$refreshToken = (string)($tokenInfo['refresh_token'] ?? '');
if (!$refreshToken) {
$refreshToken = trim((string)env('JST_REFRESH_TOKEN', ''));
}
if ($refreshToken) {
$newTokenInfo = $this->refreshAccessToken($refreshToken);
$this->writeTokenInfo($newTokenInfo);
return (string)$newTokenInfo['access_token'];
}
$code = (string)env('JST_AUTH_CODE', '');
if ($code) {
$newTokenInfo = $this->code2accessToken($code);
$this->writeTokenInfo($newTokenInfo);
return (string)$newTokenInfo['access_token'];
}
throw new ValidateException('JST token missing');
}
private function autoInitTokenFile(): ?array
{
$accessToken = (string)env('JST_ACCESS_TOKEN', '');
$refreshToken = (string)env('JST_REFRESH_TOKEN', '');
$expiresIn = (int)env('JST_EXPIRES_IN', 2592000);
if (empty($accessToken)) {
return null;
}
$tokenInfo = [
'access_token' => $accessToken,
'expires_in' => $expiresIn,
'refresh_token' => $refreshToken,
'time' => time(),
];
$this->writeTokenInfo($tokenInfo);
return $tokenInfo;
}
private function refreshAccessToken(string $refreshToken): array
{
$params = [];
$params['app_key'] = $this->api['appid'];
$params['timestamp'] = time();
$params['grant_type'] = 'refresh_token';
$params['charset'] = 'utf-8';
$params['refresh_token'] = $refreshToken;
$params['scope'] = 'all';
$params['sign'] = $this->sign($params);
$url = "{$this->api['api']}openWeb/auth/refreshToken";
$raw = file_get_contents($url, false, stream_context_create([
'http' => [
'timeout' => 60,
'method' => 'POST',
'header' => 'Content-Type: application/x-www-form-urlencoded;charset=UTF-8',
'content' => http_build_query($params),
],
]));
$rt = json_decode((string)$raw, true);
if (!is_array($rt) || !array_key_exists('code', $rt)) {
throw new ValidateException('JST response invalid');
}
if ((int)$rt['code'] !== 0) {
throw new ValidateException((string)($rt['msg'] ?? 'JST error'));
}
$data = $rt['data'] ?? [];
if (!is_array($data) || empty($data['access_token'])) {
throw new ValidateException('JST token invalid');
}
return $data;
}
private function code2accessToken(string $code): array
{
$params = [];
$params['app_key'] = $this->api['appid'];
$params['timestamp'] = time();
$params['grant_type'] = 'authorization_code';
$params['charset'] = 'utf-8';
$params['code'] = $code;
$params['sign'] = $this->sign($params);
$url = "{$this->api['api']}openWeb/auth/accessToken";
$raw = file_get_contents($url, false, stream_context_create([
'http' => [
'timeout' => 60,
'method' => 'POST',
'header' => 'Content-Type: application/x-www-form-urlencoded;charset=UTF-8',
'content' => http_build_query($params),
],
]));
$rt = json_decode((string)$raw, true);
if (!is_array($rt) || !array_key_exists('code', $rt)) {
throw new ValidateException('JST response invalid');
}
if ((int)$rt['code'] !== 0) {
throw new ValidateException((string)($rt['msg'] ?? 'JST error'));
}
$data = $rt['data'] ?? [];
if (!is_array($data) || empty($data['access_token'])) {
throw new ValidateException('JST token invalid');
}
return $data;
}
private function readTokenInfo(): ?array
{
$file = root_path() . $this->tokenFile;
if (!is_file($file)) {
Log::info('JST token file not found, will try to auto init from .env');
return null;
}
$raw = file_get_contents($file);
if (!$raw) return null;
$arr = json_decode((string)$raw, true);
if (!is_array($arr) || empty($arr['access_token'])) {
Log::warning('JST token file is invalid, will try to auto init from .env');
return null;
}
return $arr;
}
private function writeTokenInfo(array $tokenInfo): void
{
$dir = root_path() . 'runtime/jstdata';
if (!is_dir($dir)) {
mkdir($dir, 0777, true);
}
$tokenInfo['time'] = time();
file_put_contents(root_path() . $this->tokenFile, json_encode($tokenInfo, JSON_UNESCAPED_UNICODE));
}
}

View File

@@ -135,6 +135,12 @@ class HaichaoJstServices extends HaichaoBridge
}
}
}
Log::info('jst.orders_upload.request ' . json_encode([
'api' => 'open/jushuitan/orders/upload',
'oid' => $oid,
'shop_status' => $status,
'biz' => $biz,
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
$num = 3;
for ($i = $num; $i >= 0; $i--) {
if ($i <= 0) {
@@ -652,6 +658,26 @@ class HaichaoJstServices extends HaichaoBridge
return $data;
}
/**
* 按时间范围获取聚水潭商品列表
* @param string $startTime 开始时间 Y-m-d H:i:s
* @param string $endTime 结束时间 Y-m-d H:i:s
* @param int $pageIndex 页码
* @param int $pageSize 每页数量
* @return array
*/
public function getProductListByTime(string $startTime, string $endTime, int $pageIndex = 1, int $pageSize = 50): array
{
$data = $this->post('open/mall/item/query', [
'modified_begin' => $startTime,
'modified_end' => $endTime,
'page_index' => $pageIndex,
'page_size' => $pageSize,
'date_field' => '',
])['data'];
return $data;
}
public function getProductListStand(): array
{
$time = 10;
@@ -704,6 +730,94 @@ class HaichaoJstServices extends HaichaoBridge
return $data;
}
public function uploadItemSku(array $items): array
{
return $this->post('open/jushuitan/itemsku/upload', [
'items' => $items,
]);
}
public function uploadItemSkuByProductIds(array $productIds): array
{
$productIds = array_values(array_unique(array_map('intval', $productIds)));
if (!$productIds) {
throw new ValidateException('请选择商品');
}
$products = \app\model\product\product\StoreProduct::whereIn('id', $productIds)
->where('is_del', 0)
->with(['attrValue'])
->select()
->toArray();
$items = [];
$errors = [];
foreach ($products as $product) {
$iId = trim((string)($product['code'] ?? ''));
$productId = (int)($product['id'] ?? 0);
if (!$iId) {
$errors[] = [
'product_id' => $productId,
'msg' => '商品编码(code)为空',
];
continue;
}
$skuList = $product['attr_value'] ?? ($product['attrValue'] ?? []);
if (!is_array($skuList) || !$skuList) {
$errors[] = [
'product_id' => $productId,
'i_id' => $iId,
'msg' => '商品规格为空',
];
continue;
}
$productImage = (string)($product['image'] ?? '');
$productName = (string)($product['store_name'] ?? '');
$enabled = ((int)($product['is_show'] ?? 0) === 1) ? 1 : -1;
foreach ($skuList as $sku) {
$skuId = trim((string)($sku['code'] ?? ''));
if (!$skuId) {
$errors[] = [
'product_id' => $productId,
'i_id' => $iId,
'msg' => '规格编码(code)为空',
];
continue;
}
$skuImage = (string)($sku['image'] ?? $productImage);
$propertiesValue = (string)($sku['suk'] ?? '');
$propertiesValue = str_replace(',', ';', $propertiesValue);
$items[] = [
'i_id' => $iId,
'sku_id' => $skuId,
'name' => $productName,
'properties_value' => $propertiesValue,
's_price' => (float)($sku['price'] ?? ($product['price'] ?? 0)),
'market_price' => (float)($sku['ot_price'] ?? ($product['ot_price'] ?? 0)),
'c_price' => (float)($sku['cost'] ?? ($product['cost'] ?? 0)),
'pic' => $productImage,
'pic_big' => $productImage,
'sku_pic' => $skuImage,
'enabled' => $enabled,
];
}
}
if (!$items) {
throw new ValidateException('没有可同步的商品规格');
}
$rt = $this->uploadItemSku($items);
return [
'upload_count' => count($items),
'errors' => $errors,
'jst' => $rt,
];
}
public function getStork(string $ids): array
{
return $this->post('open/inventory/query', [
@@ -758,28 +872,42 @@ class HaichaoJstServices extends HaichaoBridge
return $result;
}
public function getInneropenStockAndDetailList($styleCodes): array
public function getInneropenStockAndDetailList(array|string $styleCodes, ?array &$stats = null): array
{
$rt = [];
$codeList = is_array($styleCodes) ? $styleCodes : explode(',', trim($styleCodes, ", \t\n\r\0\x0B"));
$codeList = array_values(array_filter(array_map('trim', $codeList), static fn($v) => $v !== ''));
foreach (array_chunk($codeList, 100) as $chunkCodes) {
if ($stats !== null) {
$stats['chunk_count'] = (int)($stats['chunk_count'] ?? 0) + 1;
$stats['request_sku_count'] = (int)($stats['request_sku_count'] ?? 0) + count($chunkCodes);
}
$i = 1;
do {
if ($stats !== null) {
$stats['api_call_count'] = (int)($stats['api_call_count'] ?? 0) + 1;
}
$result = $this->post('open/inventory/query', [
'page_index' => $i++,
'page_size' => 100,
'sku_ids' => $styleCodes
'sku_ids' => implode(',', $chunkCodes)
])['data']['inventorys'] ?? [];
if ($stats !== null) {
$stats['api_resp_row_count'] = (int)($stats['api_resp_row_count'] ?? 0) + count($result);
}
// 适配原来的返回格式结构
$formatResult = [];
foreach ($result as $item) {
$formatResult[] = [
'item_code' => $item['sku_id'],
'stock' => max(0, ($item['qty'] ?? 0) - ($item['order_lock'] ?? 0)) // 实际可用库存
'stock' => max(0, ($item['qty'] ?? 0) - ($item['order_lock'] ?? 0))
];
}
$rt = array_merge($formatResult, $rt);
usleep(550);
usleep(550000);
} while (count($result) == 100);
}
return $rt;
}

View File

@@ -13,31 +13,82 @@ class JstProductEvent implements JstEvent
{
try {
$param = $request->param();
$body = json_decode((string)($param['biz'] ?? '{}'), true);
$actionCode = (string)($param['action_code'] ?? '');
$parseFrom = 'param.biz';
$bizRaw = (string)($param['biz'] ?? '');
$body = json_decode($bizRaw !== '' ? $bizRaw : '{}', true);
if (!is_array($body)) {
Log::warning('jst.cb.product_stock.parse_fail ' . json_encode([
'action_code' => $actionCode,
'from' => $parseFrom,
'biz_raw_len' => strlen($bizRaw),
], JSON_UNESCAPED_UNICODE));
$body = [];
}
$skuId = (string)($body['skuId'] ?? '');
$stock = (int)($body['qty'] ?? 0);
if (!$skuId) {
$cont = $request->getContent();
$output = [];
parse_str($cont, $output);
$body = json_decode((string)($output['biz'] ?? '{}'), true);
$actionCode = (string)($output['action_code'] ?? $actionCode);
$parseFrom = 'content.parse_str.biz';
$bizRaw = (string)($output['biz'] ?? '');
$body = json_decode($bizRaw !== '' ? $bizRaw : '{}', true);
if (!is_array($body)) {
Log::warning('jst.cb.product_stock.parse_fail ' . json_encode([
'action_code' => $actionCode,
'from' => $parseFrom,
'biz_raw_len' => strlen($bizRaw),
'content_len' => strlen((string)$cont),
], JSON_UNESCAPED_UNICODE));
$body = [];
}
$skuId = (string)($body['skuId'] ?? '');
$stock = (int)($body['qty'] ?? 0);
}
if (!$skuId) return [];
if (!$skuId) {
Log::warning('jst.cb.product_stock.miss ' . json_encode([
'action_code' => $actionCode,
'from' => $parseFrom,
'sku_id' => $skuId,
], JSON_UNESCAPED_UNICODE));
return [];
}
/** @var StoreProductAttrValueServices $spas */
$spas = app()->make(StoreProductAttrValueServices::class);
$model = $spas->getOne(['code' => $skuId]);
if ($model) {
if (!$model) {
Log::warning('jst.cb.product_stock.not_found ' . json_encode([
'action_code' => $actionCode,
'from' => $parseFrom,
'sku_id' => $skuId,
'stock' => $stock,
], JSON_UNESCAPED_UNICODE));
return [];
}
$pdt = $model->toArray();
$id = $pdt['id'] ?? 0;
if ($id) {
$id = (int)($pdt['id'] ?? 0);
if (!$id) {
Log::warning('jst.cb.product_stock.not_found ' . json_encode([
'action_code' => $actionCode,
'from' => $parseFrom,
'sku_id' => $skuId,
'stock' => $stock,
], JSON_UNESCAPED_UNICODE));
return [];
}
$spas->update($id, [
'stock' => $stock,
'sum_stock' => $stock
]);
}
}
Log::info('jst.cb.product_stock.updated ' . json_encode([
'action_code' => $actionCode,
'from' => $parseFrom,
'attr_value_id' => $id,
'sku_id' => $skuId,
'stock' => $stock,
], JSON_UNESCAPED_UNICODE));
} catch (Throwable $e) {
Log::error('jst.cb.product_stock.error ' . json_encode([
'msg' => $e->getMessage(),

View File

@@ -157,6 +157,7 @@ class JstProductSync extends HaichaoJstServices
} else {
$productServices->update($pidNow, $productInfo);
}
$productServices->ensureSpu((int)$pidNow);
ProductStockTips::dispatch([$pidNow, 0]);
$attr = [[
'value' => '规格',
@@ -324,6 +325,7 @@ class JstProductSync extends HaichaoJstServices
} else {
$productServices->update($pid, $productInfo);
}
$productServices->ensureSpu((int)$pid);
ProductStockTips::dispatch([$pid, 0]);
$attr = [[
'value' => '规格',

View File

@@ -16,45 +16,76 @@ class JstStorkSync extends HaichaoJstServices
{
public function syncStorkFromInneropenErp(): void
{
$startAt = microtime(true);
/** @var StoreProductAttrValueServices $storeProductAttrValueServices */
$storeProductAttrValueServices = app()->make(StoreProductAttrValueServices::class);
Log::info('jst.stork.sync.start');
$inventorySkuIds = [];
$skuIds = '';
$index = 1;
$count = 0;
$codes = [];
$spuIds = [];
$codeIdMap = [];
$codeProductIdMap = [];
foreach ($storeProductAttrValueServices->getList(['is_del' => 0]) as $item) {
if ($item['code']) {
$skuIds .= $item['code'] . ',';
$count++;
$inventorySkuIds[$index] = [
'length' => $count,
'ids' => $skuIds,
];
if ($count >= 500) {
$skuIds = '';
$index++;
$count = 0;
$code = (string)($item['code'] ?? '');
if ($code !== '') {
$codes[] = $code;
$id = (int)($item['id'] ?? 0);
if ($id > 0) {
$codeIdMap[$code] = $id;
}
$productId = (int)($item['product_id'] ?? 0);
if ($productId > 0) {
$spuIds[$productId] = 1;
$codeProductIdMap[$code] = $productId;
}
}
}
$productStock = [];
foreach ($inventorySkuIds as $inventorySkuId) {
foreach ($this->getInneropenStockAndDetailList($inventorySkuId['ids']) as $product) {
$productStock[] = [
'code' => $product['item_code'],
'stock' => $product['stock']
];
$codes = array_values(array_unique($codes));
Log::info('jst.stork.sync.request_stats ' . json_encode([
'spu_count' => count($spuIds),
'sku_count' => count($codes),
], JSON_UNESCAPED_UNICODE));
$pullStats = [];
$productStockMap = [];
foreach ($this->getInneropenStockAndDetailList($codes, $pullStats) as $product) {
$productStockMap[(string)$product['item_code']] = (int)$product['stock'];
}
$respSpuIds = [];
foreach (array_keys($productStockMap) as $code) {
if (isset($codeProductIdMap[$code])) {
$respSpuIds[$codeProductIdMap[$code]] = 1;
}
}
Log::info('jst.stork.sync.merge ' . count($productStock));
foreach ($productStock as $item) {
$storeProductAttrValueServices->update(['code' => $item['code']], [
'stock' => $item['stock'],
'sum_stock' => $item['stock']
Log::info('jst.stork.sync.response_stats ' . json_encode([
'spu_count' => count($respSpuIds),
'sku_count' => count($productStockMap),
'api_call_count' => (int)($pullStats['api_call_count'] ?? 0),
'api_resp_row_count' => (int)($pullStats['api_resp_row_count'] ?? 0),
], JSON_UNESCAPED_UNICODE));
$updatedSkuCount = 0;
$notFoundSkuCount = 0;
foreach ($productStockMap as $code => $stock) {
$id = (int)($codeIdMap[$code] ?? 0);
if ($id < 1) {
$notFoundSkuCount++;
continue;
}
$storeProductAttrValueServices->update($id, [
'stock' => $stock,
'sum_stock' => $stock
]);
$updatedSkuCount++;
}
Log::info('jst.stork.sync.end');
Log::info('jst.stork.sync.finish ' . json_encode([
'requested_spu_count' => count($spuIds),
'requested_sku_count' => count($codes),
'response_spu_count' => count($respSpuIds),
'response_sku_count' => count($productStockMap),
'update_sku_count' => $updatedSkuCount,
'not_found_sku_count' => $notFoundSkuCount,
'duration_ms' => (int)round((microtime(true) - $startAt) * 1000),
], JSON_UNESCAPED_UNICODE));
}
public function syncStockFromErp(array $ids): bool

View File

@@ -42,8 +42,9 @@ class AllowOriginMiddleware implements MiddlewareInterface
$header = Config::get('cookie.header');
$origin = $request->header('origin');
if ($origin && ('' == $this->cookieDomain || strpos($origin, $this->cookieDomain)))
if ($origin && ('' === $this->cookieDomain || str_contains($origin, $this->cookieDomain))) {
$header['Access-Control-Allow-Origin'] = $origin;
}
if ($request->method(true) == 'OPTIONS') {
$response = Response::create('ok')->code(200)->header($header);
} else {

View File

@@ -8,6 +8,7 @@ use app\services\product\sku\StoreProductAttrValueServices;
use app\haichao\HaichaoJstServices;
use crmeb\basic\BaseJobs;
use crmeb\exceptions\AdminException;
use crmeb\services\erp\Erp as erpServices;
use crmeb\traits\QueueTrait;
use think\facade\Log;
@@ -42,7 +43,7 @@ class ProductSyncErp extends BaseJobs
}
foreach ($attrs as $item) {
if ($item['pic'] && strstr($item['pic'], 'http') === false) {
$siteUrl = sys_config('site_url');
$siteUrl = app()->make(\crmeb\services\SystemConfigService::class)->get('site_url');
$item['pic'] = $siteUrl . $item['pic'];
}
@@ -119,10 +120,18 @@ class ProductSyncErp extends BaseJobs
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
*/
public function productFromErp($spuArr, int $supplierId = 0)
public function productFromErp($spuArr, int $supplierId = 0, string $traceId = '')
{
$stats = [
'trace_id' => $traceId,
'fetched' => 0,
'created' => 0,
'updated' => 0,
'skipped' => 0,
'failed' => 0,
];
try {
Log::info('admin.manual.jst.product.sync.request', ['spu' => $spuArr]);
Log::info('admin.manual.jst.product.sync.request', ['trace_id' => $traceId, 'spu' => $spuArr, 'supplier_id' => $supplierId]);
$result = app()->make(HaichaoJstServices::class)->getProductListKuan($spuArr);
$resultPreview = null;
try {
@@ -130,6 +139,7 @@ class ProductSyncErp extends BaseJobs
} catch (\Throwable $e) {
}
Log::info('admin.manual.jst.product.sync.api_result', [
'trace_id' => $traceId,
'spu' => $spuArr,
'page_index' => $result['page_index'] ?? null,
'page_count' => $result['page_count'] ?? null,
@@ -137,20 +147,311 @@ class ProductSyncErp extends BaseJobs
'result_preview' => $resultPreview,
]);
$productList = $result['datas'] ?? [];
$productInfo = [];
$stats['fetched'] = is_array($productList) ? count($productList) : 0;
$stats = $this->mergeStats($stats, $this->processProductList($productList, $supplierId, $traceId, [
'mode' => 'spu',
'spu' => $spuArr,
]));
/** @var StoreProductServices $productServices */
$productServices = app()->make(StoreProductServices::class);
/** @var StoreProductAttrServices $productAttrServices */
$productAttrServices = app()->make(StoreProductAttrServices::class);
$productServices->cacheTag()->clear();
$productAttrServices->cacheTag()->clear();
Log::info('admin.manual.jst.product.sync.finish', [
'trace_id' => $traceId,
'spu' => $spuArr,
'count' => is_array($productList) ? count($productList) : 0,
'created' => $stats['created'],
'updated' => $stats['updated'],
'skipped' => $stats['skipped'],
'failed' => $stats['failed'],
]);
return $stats;
} catch (\Exception $e) {
response_log_write([
'message' => '商品同步失败, 原因: ' . $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine()
]);
Log::error('admin.manual.jst.product.sync.error', ['trace_id' => $traceId, 'spu' => $spuArr, 'message' => $e->getMessage()]);
$stats['failed']++;
return $stats;
}
}
private function mergeStats(array $base, array $delta): array
{
foreach (['fetched', 'created', 'updated', 'skipped', 'failed'] as $key) {
$base[$key] = (int)($base[$key] ?? 0) + (int)($delta[$key] ?? 0);
}
return $base;
}
private function processProductList($productList, int $supplierId, string $traceId, array $context = []): array
{
$stats = [
'fetched' => 0,
'created' => 0,
'updated' => 0,
'skipped' => 0,
'failed' => 0,
];
if (!is_array($productList)) {
return $stats;
}
$stats['fetched'] = count($productList);
$seenCodes = [];
foreach ($productList as $item) {
if (!is_array($item)) {
$stats['skipped']++;
continue;
}
$code = (string)($item['i_id'] ?? '');
if ($code === '') {
$stats['skipped']++;
continue;
}
if (isset($seenCodes[$code])) {
$stats['skipped']++;
continue;
}
$seenCodes[$code] = 1;
try {
$save = $this->saveProductFromErpData($item, $supplierId);
if (!empty($save['created'])) {
$stats['created']++;
} else {
$stats['updated']++;
}
} catch (\Throwable $e) {
$stats['failed']++;
Log::error('admin.manual.jst.product.sync.item.error', [
'trace_id' => $traceId,
'i_id' => $code,
'message' => $e->getMessage(),
'context' => $context,
]);
}
}
return $stats;
}
/**
* 按时间范围同步商品
* @param string $startTime
* @param string $endTime
* @param int $supplierId
* @return bool
*/
public function syncProductByTime(string $startTime, string $endTime, int $supplierId = 0, string $traceId = '', string $mode = 'dispatch')
{
$startTs = strtotime($startTime);
$endTs = strtotime($endTime);
if ($startTs && $endTs) {
$maxSeconds = 7 * 86400;
if (($endTs - $startTs) > $maxSeconds) {
Log::warning('admin.manual.jst.product.sync_by_time.range_too_large', [
'trace_id' => $traceId,
'start_time' => $startTime,
'end_time' => $endTime,
'range_seconds' => $endTs - $startTs,
'max_seconds' => $maxSeconds,
'supplier_id' => $supplierId,
]);
return [
'ok' => false,
'error' => '时间范围不能超过7天',
'trace_id' => $traceId,
];
}
}
try {
$stats = [
'ok' => true,
'mode' => $mode,
'trace_id' => $traceId,
'start_time' => $startTime,
'end_time' => $endTime,
'page_count' => 1,
'pages_processed' => 0,
'fetched' => 0,
'created' => 0,
'updated' => 0,
'skipped' => 0,
'failed' => 0,
];
Log::info('admin.manual.jst.product.sync_by_time.start', [
'trace_id' => $traceId,
'start_time' => $startTime,
'end_time' => $endTime,
'supplier_id' => $supplierId
]);
$jstService = app()->make(HaichaoJstServices::class);
$result = $jstService->getProductListByTime($startTime, $endTime, 1, 50);
$pageCount = $result['page_count'] ?? 1;
$stats['page_count'] = (int)$pageCount;
Log::info('admin.manual.jst.product.sync_by_time.api_result', [
'trace_id' => $traceId,
'page' => 1,
'page_count' => $pageCount,
'datas_count' => isset($result['datas']) && is_array($result['datas']) ? count($result['datas']) : 0,
]);
$pageStats = $this->processProductList($result['datas'] ?? [], $supplierId, $traceId, [
'mode' => 'time',
'page' => 1,
]);
$stats = $this->mergeStats($stats, $pageStats);
$stats['pages_processed']++;
if ($pageCount > 1) {
if ($mode === 'sync') {
for ($i = 2; $i <= $pageCount; $i++) {
$pageResult = $jstService->getProductListByTime($startTime, $endTime, $i, 50);
$productList = $pageResult['datas'] ?? [];
$resultPreview = null;
try {
$resultPreview = mb_substr(json_encode($pageResult, JSON_UNESCAPED_UNICODE), 0, 2000);
} catch (\Throwable $e) {
}
Log::info('admin.manual.jst.product.sync_page.api_result', [
'trace_id' => $traceId,
'page' => $i,
'page_count' => $pageResult['page_count'] ?? null,
'datas_count' => is_array($productList) ? count($productList) : 0,
'result_preview' => $resultPreview,
]);
$stats = $this->mergeStats($stats, $this->processProductList($productList, $supplierId, $traceId, [
'mode' => 'time',
'page' => $i,
]));
$stats['pages_processed']++;
}
} else {
for ($i = 2; $i <= $pageCount; $i++) {
self::dispatchDo('syncProductPage', [$startTime, $endTime, $i, $supplierId, $traceId]);
}
}
}
/** @var StoreProductServices $productServices */
$productServices = app()->make(StoreProductServices::class);
/** @var StoreProductAttrServices $productAttrServices */
$productAttrServices = app()->make(StoreProductAttrServices::class);
$productServices->cacheTag()->clear();
$productAttrServices->cacheTag()->clear();
Log::info('admin.manual.jst.product.sync_by_time.finish', [
'trace_id' => $traceId,
'start_time' => $startTime,
'end_time' => $endTime,
'supplier_id' => $supplierId,
'page_count' => $stats['page_count'],
'pages_processed' => $stats['pages_processed'],
'fetched' => $stats['fetched'],
'created' => $stats['created'],
'updated' => $stats['updated'],
'skipped' => $stats['skipped'],
'failed' => $stats['failed'],
]);
return $stats;
} catch (\Exception $e) {
Log::error('admin.manual.jst.product.sync_by_time.error', [
'trace_id' => $traceId,
'message' => $e->getMessage(),
'start_time' => $startTime,
'end_time' => $endTime
]);
return [
'ok' => false,
'error' => $e->getMessage(),
'trace_id' => $traceId,
];
}
}
/**
* 同步特定页码的商品
* @param string $startTime
* @param string $endTime
* @param int $pageIndex
* @param int $supplierId
* @return bool
*/
public function syncProductPage(string $startTime, string $endTime, int $pageIndex, int $supplierId = 0, string $traceId = '')
{
try {
Log::info('admin.manual.jst.product.sync_page.start', [
'trace_id' => $traceId,
'page' => $pageIndex,
'start_time' => $startTime,
'end_time' => $endTime
]);
$jstService = app()->make(HaichaoJstServices::class);
$result = $jstService->getProductListByTime($startTime, $endTime, $pageIndex, 50);
$productList = $result['datas'] ?? [];
$resultPreview = null;
try {
$resultPreview = mb_substr(json_encode($result, JSON_UNESCAPED_UNICODE), 0, 2000);
} catch (\Throwable $e) {
}
Log::info('admin.manual.jst.product.sync_page.api_result', [
'trace_id' => $traceId,
'page' => $pageIndex,
'page_count' => $result['page_count'] ?? null,
'datas_count' => is_array($productList) ? count($productList) : 0,
'result_preview' => $resultPreview,
]);
$stats = $this->processProductList($productList, $supplierId, $traceId, [
'mode' => 'time',
'page' => $pageIndex,
]);
/** @var StoreProductServices $productServices */
$productServices = app()->make(StoreProductServices::class);
/** @var StoreProductAttrServices $productAttrServices */
$productAttrServices = app()->make(StoreProductAttrServices::class);
$productServices->cacheTag()->clear();
$productAttrServices->cacheTag()->clear();
Log::info('admin.manual.jst.product.sync_page.finish', [
'trace_id' => $traceId,
'page' => $pageIndex,
'count' => is_array($productList) ? count($productList) : 0,
'created' => $stats['created'] ?? 0,
'updated' => $stats['updated'] ?? 0,
'skipped' => $stats['skipped'] ?? 0,
'failed' => $stats['failed'] ?? 0,
]);
return true;
} catch (\Exception $e) {
Log::error('admin.manual.jst.product.sync_page.error', [
'trace_id' => $traceId,
'page' => $pageIndex,
'message' => $e->getMessage()
]);
return false;
}
}
/**
* 保存从ERP获取的商品数据
* @param array $item
* @param int $supplierId
*/
private function saveProductFromErpData(array $item, int $supplierId = 0): array
{
/** @var StoreProductServices $productServices */
$productServices = app()->make(StoreProductServices::class);
/** @var StoreProductAttrServices $productAttrServices */
$productAttrServices = app()->make(StoreProductAttrServices::class);
Log::info('admin.manual.jst.product.sync.response', [
'spu' => $spuArr,
'datas_count' => is_array($productList) ? count($productList) : 0
]);
$relationSupplierId = $supplierId > 0 ? $supplierId : 0;
foreach ($productList as $item) {
Log::info('admin.manual.jst.product.sync.item', [
'spu' => $spuArr,
Log::info('admin.manual.jst.product.sync.item.process', [
'i_id' => $item['i_id'] ?? '',
'name' => $item['name'] ?? '',
'sku_count' => isset($item['skus']) && is_array($item['skus']) ? count($item['skus']) : 0
@@ -288,6 +589,7 @@ class ProductSyncErp extends BaseJobs
if (!$pid) {
$pid = $productServices->ErpProductSave($productInfo);
}
$productServices->ensureSpu((int)$pid);
$attr = [[
'value' => '规格',
'detail' => $detail,
@@ -297,30 +599,11 @@ class ProductSyncErp extends BaseJobs
$skuList = $productAttrServices->validateProductAttr($attr, $value, $pid, 0, 0, 0);
$productAttrServices->saveProductAttr($skuList, $pid);
Log::info('admin.manual.jst.product.sync.save', [
'spu' => $spuArr,
'product_id' => $pid,
'existed' => $existed,
'sku_saved_count' => isset($skuList['value']) && is_array($skuList['value']) ? count($skuList['value']) : 0
]);
}
//清除数据缓存
$productServices->cacheTag()->clear();
$productAttrServices->cacheTag()->clear();
Log::info('admin.manual.jst.product.sync.finish', ['spu' => $spuArr, 'count' => count($productList)]);
} catch (\Exception $e) {
response_log_write([
'message' => '商品同步失败, 原因: ' . $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine()
]);
Log::error('admin.manual.jst.product.sync.error', ['spu' => $spuArr, 'message' => $e->getMessage()]);
}
return true;
return ['created' => !$existed, 'product_id' => $pid, 'existed' => $existed];
}
/**
@@ -412,7 +695,7 @@ class ProductSyncErp extends BaseJobs
}
// 更新平台商品库存
$defaultShopId = (int)sys_config('jst_default_shopid');
$defaultShopId = (int)app()->make(\crmeb\services\SystemConfigService::class)->get('jst_default_shopid');
if ($defaultShopId == $erpShopId) {
$this->updateStoreProductValueStock($data);
}

View File

@@ -80,6 +80,12 @@ class OrderServices
$biz = array_merge($biz, $this->getAddress($order['user_address']));
Log::info('jst.orders_upload.request ' . json_encode([
'api' => 'open/jushuitan/orders/upload',
'oid' => $oid,
'biz' => $biz,
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
$num = 3;
for ($i = $num; $i >= 0; $i--) {
if ($i <= 0) {
@@ -544,7 +550,7 @@ class OrderServices
public function getShopId(int $erpShopId): int
{
if ($erpShopId < 1) {
$erpShopId = sys_config('jst_default_shopid');
$erpShopId = (int)app('sysConfig')->get('jst_default_shopid');
}
return (int)$erpShopId;

View File

@@ -727,6 +727,19 @@ class StoreOrderRefundServices extends BaseServices
OrderStatusJob::dispatch([$order['id'], 'refund_price', ['change_message' => '退款给用户:' . bcadd($online_refund_amount, $yue_refund_amount, 2) . '元; ' . $admin, 'change_manager_id' => request()->adminId(), 'change_manager_type' => 'admin']]);
return $order;
});
if ($order['private_bind_id'] && (int)$order['private_phone_status'] === 1 && $order['user_phone']) {
try {
$privacyPhoneServices = app()->make(\app\services\other\PrivacyPhoneServices::class);
if ($privacyPhoneServices->unbindForOrder((string)$order['user_phone'], (string)$order['private_bind_id'])) {
$this->storeOrderServices->update($order['id'], ['private_phone_status' => 0]);
}
} catch (\Throwable $e) {
\think\facade\Log::error('退款解绑隐私小号失败 order_id={order_id} error={error}', [
'order_id' => $order['id'],
'error' => $e->getMessage(),
]);
}
}
//订单同意退款事件
event('order.refund', [$refundData, $order, 'order_refund', (int)$id]);
return true;

View File

@@ -2027,6 +2027,19 @@ HTML;
throw new ValidateException('订单号' . $order['order_id'] . ',取消订单失败');
}
});
if ($order['private_bind_id'] && (int)$order['private_phone_status'] === 1 && $order['user_phone']) {
try {
$privacyPhoneServices = app()->make(\app\services\other\PrivacyPhoneServices::class);
if ($privacyPhoneServices->unbindForOrder((string)$order['user_phone'], (string)$order['private_bind_id'])) {
$this->dao->update($order['id'], ['private_phone_status' => 0]);
}
} catch (\Throwable $e) {
\think\facade\Log::error('取消订单解绑隐私小号失败 order_id={order_id} error={error}', [
'order_id' => $order['id'],
'error' => $e->getMessage(),
]);
}
}
//订单取消事件
event('order.cancel', [$order]);
return true;
@@ -2139,7 +2152,12 @@ HTML;
public function getExportList(array $where, array $with = [], int $limit = 0)
{
if ($limit) {
$page = 1;
if (!empty($where['id'])) {
$page = 1;
} else {
[$page] = $this->getPageValue();
}
} else {
[$page, $limit] = $this->getPageValue();
}
@@ -2151,10 +2169,17 @@ HTML;
foreach ($list as &$item) {
/** @var StoreOrderCartInfoServices $orderCart */
$orderCart = app()->make(StoreOrderCartInfoServices::class);
$_info = $orderCart->getCartColunm(['oid' => $item['id']], 'cart_info', 'unique');
$_info = $orderCart->getCartColunm(['oid' => $item['id']], 'cart_id,type,relation_id,is_gift,cart_num,refund_num,cart_info', 'unique');
foreach ($_info as $k => $v) {
$cart_info = is_string($v) ? json_decode($v, true) : $v;
$cart_info = is_string($v['cart_info']) ? json_decode($v['cart_info'], true) : $v['cart_info'];
if (!isset($cart_info['productInfo'])) $cart_info['productInfo'] = [];
$cart_info['cart_num'] = $v['cart_num'] ?? ($cart_info['cart_num'] ?? 0);
$cart_info['refund_num'] = $v['refund_num'] ?? 0;
$cart_info['cart_id'] = $v['cart_id'] ?? ($cart_info['id'] ?? 0);
$cart_info['_relation_type'] = $v['type'] ?? 0;
$cart_info['_relation_id'] = $v['relation_id'] ?? 0;
$cart_info['is_gift'] = $v['is_gift'] ?? ($cart_info['is_gift'] ?? 0);
$cart_info['unique'] = $k;
$_info[$k] = $cart_info;
unset($cart_info);
}

View File

@@ -23,6 +23,35 @@ use think\facade\Log;
*/
class PrivacyPhoneServices extends BaseServices
{
/**
* 标准化AX小号:从后往前截取11位手机号
* @param string $number
* @return string
*/
private function normalizeAxNumber(string $number): string
{
if ($number === '') {
return '';
}
return strlen($number) > 11 ? substr($number, -11) : $number;
}
/**
* 日志上下文转字符串(ThinkPHP日志上下文仅支持占位符替换)
* @param mixed $value
* @return string
*/
private function toLogString($value): string
{
if (is_string($value)) {
return $value;
}
$json = json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
return $json === false ? var_export($value, true) : $json;
}
/**
* API地址
*/
@@ -122,10 +151,10 @@ class PrivacyPhoneServices extends BaseServices
'decoded' => json_decode($response, true) ?? []
];
Log::info('隐私小号API请求', [
Log::info('隐私小号API请求 url={url} params={params} result={result}', [
'url' => $url,
'params' => $params,
'result' => $result
'params' => $this->toLogString($params),
'result' => $this->toLogString($result),
]);
return $result;
@@ -267,23 +296,33 @@ class PrivacyPhoneServices extends BaseServices
return $result;
}
$bindResult = $this->axBind($number, $realPhone);
$bindNumber = $this->normalizeAxNumber($number);
if (strlen($bindNumber) !== 11) {
Log::error('隐私小号格式异常 raw_number={raw_number} bind_number={bind_number}', [
'raw_number' => $number,
'bind_number' => $bindNumber,
]);
return $result;
}
$bindResult = $this->axBind($bindNumber, $realPhone);
if (!empty($bindResult['success']) && $bindResult['code'] == 200) {
$result = [
'user_phone' => $number,
'user_phone' => $bindNumber,
'bind_id' => $realPhone,
'status' => 1
];
Log::info('隐私小号绑定成功', [
'privacy_phone' => $number,
'real_phone' => $realPhone
Log::info('隐私小号绑定成功 privacy_phone={privacy_phone} real_phone={real_phone}', [
'privacy_phone' => $bindNumber,
'real_phone' => $realPhone,
]);
}
} catch (\Exception $e) {
Log::error('隐私小号绑定异常', [
Log::error('隐私小号绑定异常 real_phone={real_phone} ax_number={ax_number} error={error}', [
'real_phone' => $realPhone,
'error' => $e->getMessage()
'ax_number' => $bindNumber ?? ($number ?? ''),
'error' => $e->getMessage(),
]);
}
@@ -306,21 +345,30 @@ class PrivacyPhoneServices extends BaseServices
return false;
}
$unbindPhone = $this->normalizeAxNumber($privacyPhone);
if (strlen($unbindPhone) !== 11) {
Log::error('隐私小号格式异常 privacy_phone={privacy_phone} unbind_phone={unbind_phone}', [
'privacy_phone' => $privacyPhone,
'unbind_phone' => $unbindPhone,
]);
return false;
}
try {
$result = $this->unAxBind($privacyPhone, $realPhone);
$result = $this->unAxBind($unbindPhone, $realPhone);
if (!empty($result['success']) && $result['code'] == 200) {
Log::info('隐私小号解绑成功', [
'privacy_phone' => $privacyPhone,
'real_phone' => $realPhone
Log::info('隐私小号解绑成功 privacy_phone={privacy_phone} real_phone={real_phone}', [
'privacy_phone' => $unbindPhone,
'real_phone' => $realPhone,
]);
return true;
}
} catch (\Exception $e) {
Log::error('隐私小号解绑异常', [
'privacy_phone' => $privacyPhone,
Log::error('隐私小号解绑异常 privacy_phone={privacy_phone} real_phone={real_phone} error={error}', [
'privacy_phone' => $unbindPhone,
'real_phone' => $realPhone,
'error' => $e->getMessage()
'error' => $e->getMessage(),
]);
}

View File

@@ -14,10 +14,14 @@ namespace app\services\other\export;
use app\model\product\stock\StockRecord;
use app\services\BaseServices;
use app\jobs\system\ExportExcelJob;
use app\model\order\StoreOrderRefund;
use app\model\order\StoreOrderStatus;
use app\services\order\StoreOrderServices;
use app\services\pay\PayServices;
use app\services\product\label\StoreProductLabelServices;
use app\services\product\product\StoreDescriptionServices;
use app\services\product\sku\StoreProductAttrResultServices;
use app\services\supplier\SystemSupplierServices;
use app\services\user\UserServices;
use crmeb\services\SpreadsheetExcelService;
@@ -729,9 +733,11 @@ class ExportServices extends BaseServices
$header = ['订单ID', '订单编号', '性别', '电话', '收货人姓名', '收货人电话', '收货地址', '商品信息', '商品总数',
'总价格', '实际支付', '邮费', '会员优惠金额', '优惠卷金额', '积分抵扣金额', '支付状态', '支付时间', '订单状态', '下单时间', '用户备注'];
$header = [
'订单号', '订单类型', '订单状态', '下单时间', '买家姓名(uid)', '买家手机号', '收货地址', '商品名称', '商品数量', '商品金额合计', '邮费',
'会员优惠', '优惠券优惠', '积分抵扣', '活动优惠', '首单优惠', '改价优惠', '实付金额', '实付邮费', '支付方式',
'订单号', '订单类型', '订单状态', '下单时间', '买家姓名(uid)', '用户名称', '买家手机号', '收货地址',
'商品名称', '商品标签', 'SPU', '款式编码', '商品编号', '商品规格', '商品数量', '商品金额合计', '结算价', '商品来源',
'邮费', '会员优惠', '优惠券优惠', '积分抵扣', '活动优惠', '首单优惠', '改价优惠', '实付金额', '实付邮费', '支付方式',
'支付状态', '支付时间', '订单配送方式', '快递公司(单号)/送货人姓名(手机号)',
'退款状态', '退款金额', '分销员', '分销佣金', '订单发货时间',
// '售后状态', '售后类型', '售后件数', '退款金额', '退款时间', '退货备注'
];
$title = ['订单导出', '订单信息' . time(), ' 生成时间:' . date('Y-m-d H:i:s', time())];
@@ -744,6 +750,96 @@ class ExportServices extends BaseServices
$export = [];
$filekey = [];
if (!empty($data)) {
$userList = [];
$spreadList = [];
$orderStatusTime = [];
$refundCartMap = [];
$supplierList = [];
$labelNameMap = [];
if (!$type) {
$userServices = app()->make(UserServices::class);
$uids = array_values(array_unique(array_column($data, 'uid')));
$userList = $uids ? $userServices->getColumn([['uid', 'IN', $uids]], 'uid,nickname,spread_uid', 'uid') : [];
$spreadUids = $userList ? array_values(array_filter(array_unique(array_column($userList, 'spread_uid')))) : [];
$spreadList = $spreadUids ? $userServices->getColumn([['uid', 'IN', $spreadUids]], 'uid,nickname', 'uid') : [];
$orderIds = array_values(array_unique(array_column($data, 'id')));
$statusRows = StoreOrderStatus::whereIn('oid', $orderIds)
->whereIn('change_type', ['delivery', 'delivery_goods', 'delivery_part_split', 'delivery_split', 'delivery_fictitious'])
->field(['oid', 'change_type', 'change_time'])
->order('change_time desc')
->select()
->toArray();
foreach ($statusRows as $row) {
$oid = (int)$row['oid'];
$changeType = (string)$row['change_type'];
if (!isset($orderStatusTime[$oid][$changeType])) {
$orderStatusTime[$oid][$changeType] = (int)$row['change_time'];
}
}
$refundRows = StoreOrderRefund::whereIn('store_order_id', $orderIds)
->where('is_cancel', 0)
->where('is_del', 0)
->whereIn('refund_type', [0, 1, 2, 4, 5, 6])
->field(['store_order_id', 'refund_type', 'cart_info'])
->select()
->toArray();
foreach ($refundRows as $refund) {
$oid = (int)$refund['store_order_id'];
$refundType = (int)$refund['refund_type'];
$cartInfo = $refund['cart_info'] ?? [];
foreach ($cartInfo as $cart) {
$cartId = (int)($cart['id'] ?? 0);
$qty = (int)($cart['cart_num'] ?? 0);
if (!$cartId || !$qty) {
continue;
}
if (!isset($refundCartMap[$oid][$cartId])) {
$refundCartMap[$oid][$cartId] = ['refunding_qty' => 0, 'refunded_qty' => 0];
}
if ($refundType === 6) {
$refundCartMap[$oid][$cartId]['refunded_qty'] += $qty;
} else {
$refundCartMap[$oid][$cartId]['refunding_qty'] += $qty;
}
}
}
$supplierIds = [];
$labelIds = [];
foreach ($data as $o) {
foreach ($o['_info'] ?? [] as $cart) {
$productInfo = $cart['productInfo'] ?? [];
$supplierId = (int)($productInfo['supplier_id'] ?? 0);
if ($supplierId > 0) {
$supplierIds[$supplierId] = $supplierId;
} elseif ((int)($cart['_relation_type'] ?? 0) === 2 && (int)($cart['_relation_id'] ?? 0) > 0) {
$supplierId = (int)$cart['_relation_id'];
$supplierIds[$supplierId] = $supplierId;
}
$storeLabelId = $productInfo['store_label_id'] ?? [];
$storeLabelId = is_string($storeLabelId) ? array_map('intval', array_filter(explode(',', $storeLabelId))) : (is_array($storeLabelId) ? $storeLabelId : []);
foreach ($storeLabelId as $lid) {
$lid = (int)$lid;
if ($lid > 0) {
$labelIds[$lid] = $lid;
}
}
}
}
$supplierService = app()->make(SystemSupplierServices::class);
$supplierList = $supplierIds ? $supplierService->getColumn([['id', 'IN', array_values($supplierIds)]], 'id,supplier_name', 'id') : [];
$labelService = app()->make(StoreProductLabelServices::class);
$labelList = $labelIds ? $labelService->getLabelCache(array_values($labelIds), ['id', 'label_name'], false) : [];
foreach ($labelList as $label) {
if (isset($label['id'])) {
$labelNameMap[(int)$label['id']] = (string)($label['label_name'] ?? '');
}
}
}
$i = 0;
foreach ($data as $item) {
if (!$type) {
@@ -805,24 +901,9 @@ class ExportServices extends BaseServices
} else if ($item['paid'] == 1 && $item['refund_status'] == 3) {
$item['status_name'] = '部分退款';
}
$goodsName = [];
$vip_sum_price = 0;
foreach ($item['_info'] as $k => $v) {
$suk = '';
if (isset($v['productInfo']['attrInfo'])) {
if (isset($v['productInfo']['attrInfo']['suk'])) {
$suk = '(' . $v['productInfo']['attrInfo']['suk'] . ')';
}
}
if (isset($v['productInfo']['store_name'])) {
$goodsName[] = implode(' ',
[
$v['productInfo']['store_name'],
$suk,
"[{$v['cart_num']} * {$v['truePrice']}]",
]);
}
$vip_sum_price = bcadd((string)$vip_sum_price, bcmul($v['vip_truePrice'], $v['cart_num'] ? $v['cart_num'] : 1, 4), 2);
foreach ($item['_info'] as $v) {
$vip_sum_price = bcadd((string)$vip_sum_price, bcmul((string)($v['vip_truePrice'] ?? 0), (string)($v['cart_num'] ? $v['cart_num'] : 1), 4), 2);
}
// if ($item['sex'] == 1) $sex_name = '男';
// else if ($item['sex'] == 2) $sex_name = '女';
@@ -856,17 +937,103 @@ class ExportServices extends BaseServices
'send' => '配送',
default => '暂无',
};
$orderLevelDeliveryTime = '';
$oid = (int)$item['id'];
$timeMap = $orderStatusTime[$oid] ?? [];
$deliveryTime = $timeMap['delivery'] ?? ($timeMap['delivery_goods'] ?? ($timeMap['delivery_part_split'] ?? ($timeMap['delivery_split'] ?? ($timeMap['delivery_fictitious'] ?? 0))));
if ($deliveryTime) {
$orderLevelDeliveryTime = date('Y-m-d H:i:s', $deliveryTime);
}
$currentUser = $userList[(int)$item['uid']] ?? [];
$userNickname = (string)($currentUser['nickname'] ?? '');
$spreadUid = (int)($currentUser['spread_uid'] ?? 0);
$spreadNickname = $spreadUid ? (string)($spreadList[$spreadUid]['nickname'] ?? '') : '';
if ($spreadUid && $spreadUid === (int)$item['uid'] && $spreadNickname !== '') {
$spreadNickname .= '(自购)';
}
foreach ($item['_info'] as $v) {
$productInfo = $v['productInfo'] ?? [];
$attrInfo = $productInfo['attrInfo'] ?? [];
$storeName = (string)($productInfo['store_name'] ?? '');
$suk = (string)($attrInfo['suk'] ?? '');
$productLabelNames = [];
if (!empty($productInfo['store_label']) && is_array($productInfo['store_label'])) {
foreach ($productInfo['store_label'] as $label) {
$name = (string)($label['label_name'] ?? ($label['label'] ?? ''));
if ($name !== '') {
$productLabelNames[] = $name;
}
}
} else {
$storeLabelId = $productInfo['store_label_id'] ?? [];
$storeLabelId = is_string($storeLabelId) ? array_map('intval', array_filter(explode(',', $storeLabelId))) : (is_array($storeLabelId) ? $storeLabelId : []);
foreach ($storeLabelId as $lid) {
$name = $labelNameMap[(int)$lid] ?? '';
if ($name !== '') {
$productLabelNames[] = $name;
}
}
}
$productLabelText = $productLabelNames ? implode(',', array_values(array_unique($productLabelNames))) : '';
$supplierId = (int)($productInfo['supplier_id'] ?? 0);
if (!$supplierId && (int)($v['_relation_type'] ?? 0) === 2) {
$supplierId = (int)($v['_relation_id'] ?? 0);
}
$productSource = $supplierId ? (string)($supplierList[$supplierId]['supplier_name'] ?? '') : '平台';
$cartId = (int)($v['cart_id'] ?? 0);
$refundedQty = (int)($refundCartMap[$oid][$cartId]['refunded_qty'] ?? 0);
$refundingQty = (int)($refundCartMap[$oid][$cartId]['refunding_qty'] ?? 0);
$refundQty = $refundedQty + $refundingQty;
$cartNum = (int)($v['cart_num'] ?? 0);
$truePrice = (string)($v['truePrice'] ?? 0);
$productAmount = bcmul($truePrice, (string)$cartNum, 2);
$refundStatus = '';
if ($refundQty > 0) {
if ($refundingQty > 0) {
$refundStatus = '退款中';
} else {
$refundStatus = $refundQty >= $cartNum ? '退款成功(全额退款)' : '退款成功(部分退款)';
}
}
$refundAmount = $refundQty > 0 ? bcmul($truePrice, (string)$refundQty, 2) : '';
$settlePrice = (string)($attrInfo['settle_price'] ?? 0);
$oneBrokerage = $v['one_brokerage'] ?? null;
if ($oneBrokerage === null) {
$oneBrokerage = bcmul((string)($attrInfo['brokerage'] ?? 0), (string)$cartNum, 2);
}
$twoBrokerage = $v['two_brokerage'] ?? null;
if ($twoBrokerage === null) {
$twoBrokerage = bcmul((string)($attrInfo['brokerage_two'] ?? 0), (string)$cartNum, 2);
}
$commission = bcadd((string)$oneBrokerage, (string)$twoBrokerage, 2);
$one_data = [
'order_id' => $item['order_id'],
'order_type' => $item['pink_name'] ?? '普通订单',
'status_name' => $item['status_name'] ?? '未知状态',
'add_time' => empty($item['add_time']) ? '' : date('Y-m-d H:i:s', (int)$item['add_time']),
'real_name' => $item['real_name'] . '(' . $item['uid'] . ')',
'user_nickname' => $userNickname,
'user_phone' => $item['user_phone'],
'user_address' => $item['user_address'],
'goods_name' => $goodsName ? implode("\n", $goodsName) : '',
'total_num' => $item['total_num'],
'total_price' => $item['total_price'],
'goods_name' => $storeName,
'goods_label' => $productLabelText,
'spu' => (string)($productInfo['spu'] ?? ''),
'style_code' => (string)($productInfo['code'] ?? ''),
'sku_code' => (string)($attrInfo['code'] ?? ''),
'goods_suk' => $suk,
'cart_num' => $cartNum,
'product_amount' => $productAmount,
'settle_price' => $settlePrice,
'product_source' => $productSource,
'total_postage' => $item['total_postage'],
'vip_sum_price' => $vip_sum_price,
'coupon_price' => $item['coupon_price'],
@@ -881,7 +1048,23 @@ class ExportServices extends BaseServices
'pay_time' => empty($item['pay_time']) ? '' : date('Y-m-d H:i:s', (int)$item['pay_time']),
'delivery_type' => $delivery_type,
'delivery_info' => $item['delivery_name'] . '(' . $item['delivery_id'] . ')',
'refund_status' => $refundStatus,
'refund_amount' => $refundAmount,
'spread_nickname' => $spreadNickname,
'spread_commission' => $commission,
'delivery_time' => $orderLevelDeliveryTime,
];
if ($export_type == 1) {
$export[] = $one_data;
if ($i == 0) {
$filekey = array_keys($one_data);
}
} else {
$export[] = array_values($one_data);
}
$i++;
}
} else {
if (isset($item['pinkStatus']) && $item['pinkStatus'] != 2) {
continue;
@@ -937,6 +1120,7 @@ class ExportServices extends BaseServices
'pay_time' => $item['pay_time'] ? date('Y-m-d H:i:s', (int)$item['pay_time']) : '暂无',
];
}
if ($type) {
if ($export_type == 1) {
$export[] = $one_data;
if ($i == 0) {
@@ -948,6 +1132,7 @@ class ExportServices extends BaseServices
$i++;
}
}
}
if ($export_type == 1) {
return compact('header', 'filekey', 'export', 'filename');
} else {

View File

@@ -797,6 +797,35 @@ class StoreProductServices extends BaseServices
return substr(implode('', array_map('ord', str_split(substr(uniqid(), 7, 13), 1))), 0, 8) . str_pad((string)mt_rand(1, 99999), 5, '0', STR_PAD_LEFT);
}
protected function createUniqueSpu(): string
{
$tries = 0;
do {
$spu = $this->createSpu();
$tries++;
} while ($tries < 20 && $this->dao->be(['spu' => $spu], 'spu'));
return $spu;
}
public function ensureSpu(int $productId): string
{
$spu = (string)$this->dao->value(['id' => $productId], 'spu');
if ($spu !== '') {
return $spu;
}
$spu = $this->createUniqueSpu();
$this->update($productId, ['spu' => $spu]);
return $spu;
}
public function ErpProductSave(array $data): int
{
if (!isset($data['spu']) || $data['spu'] === '') {
$data['spu'] = $this->createUniqueSpu();
}
return (int)$this->dao->ErpProductSave($data);
}
/**
* 新增编辑商品
* @param int $id
@@ -1531,7 +1560,7 @@ class StoreProductServices extends BaseServices
}
}
[$page, $limit] = $this->getPageValue();
$field = ['id,relation_id,type,pid,delivery_type,product_type,store_name,cate_id,image,IFNULL(sales, 0) + IFNULL(ficti, 0) as sales,price,stock,activity,ot_price,spec_type,recommend_image,unit_name,is_vip,vip_price,is_presale_product,is_vip_product,system_form_id,is_presale_product,presale_start_time,presale_end_time,is_limit,limit_num,video_open,video_link,freight,star,store_label_id,brand_id,min_qty'];
$field = ['id,spu,relation_id,type,pid,delivery_type,product_type,store_name,cate_id,image,IFNULL(sales, 0) + IFNULL(ficti, 0) as sales,price,stock,activity,ot_price,spec_type,recommend_image,unit_name,is_vip,vip_price,is_presale_product,is_vip_product,system_form_id,is_presale_product,presale_start_time,presale_end_time,is_limit,limit_num,video_open,video_link,freight,star,store_label_id,brand_id,min_qty'];
$list = $this->dao->getSearchList($where, $page, $limit, $field, 'sales desc,id desc', ['couponId']);
if ($list) {
/** @var MemberCardServices $memberCardService */

View File

@@ -31,15 +31,13 @@ class OrderStatisticServices extends BaseServices
$orderService = app()->make(StoreOrderServices::class);
$data['pay_price'] = $orderService->sum(['paid' => 1, 'pid' => 0, 'time' => $where['time'], 'supplier_id' => $supplier_id], 'pay_price', true);
$data['pay_count'] = $orderService->count(['paid' => 1, 'pid' => 0, 'time' => $where['time'], 'supplier_id' => $supplier_id]);
$times = explode('-', $where['time']);
if(count($times) > 1){
$times = [
'start_time'=>$times[0],
'end_time'=>$times[1] . ' 23:59:59'
];
$refundWhere = ['refund_type' => 6, 'time' => explode('-', $where['time']), 'is_cancel' => 0, 'is_del' => 0];
if ($supplier_id !== '') {
$refundWhere['supplier_id'] = $supplier_id;
}
$data['refund_price'] = app()->make(StoreOrderRefundServices::class)->sum(['refund_type' => 6, 'time' => explode('-', $where['time'])],'refund_price', true);
$data['refund_count'] = app()->make(StoreOrderRefundServices::class)->count(['refund_type' => 6, 'time' => explode('-', $where['time'])]);
$refundService = app()->make(StoreOrderRefundServices::class);
$data['refund_price'] = $refundService->sum($refundWhere, 'refund_price', true);
$data['refund_count'] = $refundService->count($refundWhere);
$data['coupon_price'] = $orderService->sum(['paid' => 1, 'pid' => 0, 'time' => $where['time'], 'supplier_id' => $supplier_id], 'coupon_price', true);
$data['coupon_count'] = $orderService->search(['paid' => 1, 'pid' => 0, 'time' => $where['time'], 'is_coupon' => 1, 'supplier_id' => $supplier_id])->count();
return $data;

View File

@@ -394,7 +394,7 @@ class SystemAdminServices extends BaseServices
$verifyCode = CacheService::get('code_' . $data['phone']);
if (!$verifyCode)
throw new AdminException('请先获取验证码');
$verifyCode = substr($verifyCode, 0, 6);
$verifyCode = substr($verifyCode, 0, 4);
if ($verifyCode != $data['code']) {
CacheService::delete('code_' . $data['phone']);
throw new AdminException('验证码错误');

View File

@@ -65,7 +65,7 @@ class SystemAttachmentServices extends BaseServices
if ($item['file_type'] == 1) {
if ($site_url) {
$item['satt_dir'] = (strpos($item['satt_dir'], $site_url) !== false || strstr($item['satt_dir'], 'http') !== false) ? $item['satt_dir'] : $site_url . $item['satt_dir'];
$item['att_dir'] = (strpos($item['att_dir'], $site_url) !== false || strstr($item['att_dir'], 'http') !== false) ? $item['satt_dir'] : $site_url . $item['att_dir'];
$item['att_dir'] = (strpos($item['att_dir'], $site_url) !== false || strstr($item['att_dir'], 'http') !== false) ? $item['att_dir'] : $site_url . $item['att_dir'];
}
}
$item['att_size'] = formatFileSize($item['att_size']);

View File

@@ -167,7 +167,7 @@ class LoginServices extends BaseServices
if (CacheService::get('code_' . $phone))
throw new ValidateException($time . '分钟内有效');
mt_srand();
$code = rand(100000, 999999);
$code = rand(1000, 9999);
$data['code'] = $code;
$data['time'] = $time;
$res = SmsJob::dispatch([$phone, $data, 'VERIFICATION_CODE_TIME']);

View File

@@ -48,7 +48,8 @@
"joypack/tencent-map": "^1.0",
"topthink/think-throttle": "^2.0",
"topthink/think-image": "^1.0",
"endroid/qr-code": "^4.8"
"endroid/qr-code": "^4.8",
"alibabacloud/dysmsapi-20170525": "^4.5"
},
"require-dev": {
"symfony/var-dumper": ">=4.2",

283
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "2f9e40fa2073aef40ba0f8ab8cfd38df",
"content-hash": "896ca6dc319244fd3f1db248c6b9d8be",
"packages": [
{
"name": "adbario/php-dot-notation",
@@ -59,6 +59,281 @@
},
"time": "2022-10-14T20:31:46+00:00"
},
{
"name": "alibabacloud/credentials",
"version": "1.2.3",
"source": {
"type": "git",
"url": "https://github.com/aliyun/credentials-php.git",
"reference": "f6d1986e7b7be8da781d0b99f24c92d9860ba0c1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/aliyun/credentials-php/zipball/f6d1986e7b7be8da781d0b99f24c92d9860ba0c1",
"reference": "f6d1986e7b7be8da781d0b99f24c92d9860ba0c1",
"shasum": ""
},
"require": {
"adbario/php-dot-notation": "^2.2",
"alibabacloud/tea": "^3.0",
"ext-curl": "*",
"ext-json": "*",
"ext-libxml": "*",
"ext-mbstring": "*",
"ext-openssl": "*",
"ext-simplexml": "*",
"ext-xmlwriter": "*",
"guzzlehttp/guzzle": "^6.3|^7.0",
"php": ">=5.6"
},
"require-dev": {
"composer/composer": "^1.8",
"drupal/coder": "^8.3",
"ext-dom": "*",
"ext-pcre": "*",
"ext-sockets": "*",
"ext-spl": "*",
"mikey179/vfsstream": "^1.6",
"monolog/monolog": "^1.24",
"phpunit/phpunit": "^5.7|^6.6|^9.3",
"psr/cache": "^1.0",
"symfony/dotenv": "^3.4",
"symfony/var-dumper": "^3.4"
},
"suggest": {
"ext-sockets": "To use client-side monitoring"
},
"type": "library",
"autoload": {
"psr-4": {
"AlibabaCloud\\Credentials\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"Apache-2.0"
],
"authors": [
{
"name": "Alibaba Cloud SDK",
"email": "sdk-team@alibabacloud.com",
"homepage": "http://www.alibabacloud.com"
}
],
"description": "Alibaba Cloud Credentials for PHP",
"homepage": "https://www.alibabacloud.com/",
"keywords": [
"alibaba",
"alibabacloud",
"aliyun",
"client",
"cloud",
"credentials",
"library",
"sdk",
"tool"
],
"support": {
"issues": "https://github.com/aliyun/credentials-php/issues",
"source": "https://github.com/aliyun/credentials-php"
},
"time": "2025-04-18T09:09:46+00:00"
},
{
"name": "alibabacloud/darabonba",
"version": "v1.0.4",
"source": {
"type": "git",
"url": "https://github.com/alibabacloud-sdk-php/darabonba.git",
"reference": "b1ccea693258ea68e455e330922406f3afe10e9c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/alibabacloud-sdk-php/darabonba/zipball/b1ccea693258ea68e455e330922406f3afe10e9c",
"reference": "b1ccea693258ea68e455e330922406f3afe10e9c",
"shasum": ""
},
"require": {
"adbario/php-dot-notation": "^2.4",
"alibabacloud/tea": "^3.2",
"ext-curl": "*",
"ext-json": "*",
"ext-libxml": "*",
"ext-mbstring": "*",
"ext-openssl": "*",
"ext-simplexml": "*",
"ext-xmlwriter": "*",
"guzzlehttp/guzzle": "^6.3|^7.0",
"php": ">=5.5"
},
"require-dev": {
"phpunit/phpunit": "^4.8.35|^5.4.3|^9.3",
"symfony/dotenv": "^3.4",
"symfony/var-dumper": "^3.4"
},
"suggest": {
"ext-sockets": "To use client-side monitoring"
},
"type": "library",
"autoload": {
"psr-4": {
"AlibabaCloud\\Dara\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"Apache-2.0"
],
"authors": [
{
"name": "Alibaba Cloud SDK",
"email": "sdk-team@alibabacloud.com",
"homepage": "http://www.alibabacloud.com"
}
],
"description": "Client of Darabonba for PHP",
"homepage": "https://www.alibabacloud.com/",
"keywords": [
"alibabacloud",
"client",
"cloud",
"tea"
],
"support": {
"issues": "https://github.com/aliyun/tea-php/issues",
"source": "https://github.com/aliyun/tea-php"
},
"time": "2025-12-15T10:15:24+00:00"
},
{
"name": "alibabacloud/dysmsapi-20170525",
"version": "4.5.1",
"source": {
"type": "git",
"url": "https://github.com/alibabacloud-sdk-php/Dysmsapi-20170525.git",
"reference": "1361ecae87674e883cc6e1251033693e3ba0e38d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/alibabacloud-sdk-php/Dysmsapi-20170525/zipball/1361ecae87674e883cc6e1251033693e3ba0e38d",
"reference": "1361ecae87674e883cc6e1251033693e3ba0e38d",
"shasum": ""
},
"require": {
"alibabacloud/darabonba": "^1.0.0",
"alibabacloud/openapi-core": "^1.0.0",
"php": ">5.5"
},
"type": "library",
"autoload": {
"psr-4": {
"AlibabaCloud\\SDK\\Dysmsapi\\V20170525\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"Apache-2.0"
],
"authors": [
{
"name": "Alibaba Cloud SDK",
"email": "sdk-team@alibabacloud.com"
}
],
"description": "Alibaba Cloud Dysmsapi (20170525) SDK Library for PHP",
"support": {
"source": "https://github.com/alibabacloud-sdk-php/Dysmsapi-20170525/tree/4.5.1"
},
"time": "2026-04-07T18:19:34+00:00"
},
{
"name": "alibabacloud/gateway-spi",
"version": "1.0.0",
"source": {
"type": "git",
"url": "https://github.com/alibabacloud-sdk-php/alibabacloud-gateway-spi.git",
"reference": "7440f77750c329d8ab252db1d1d967314ccd1fcb"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/alibabacloud-sdk-php/alibabacloud-gateway-spi/zipball/7440f77750c329d8ab252db1d1d967314ccd1fcb",
"reference": "7440f77750c329d8ab252db1d1d967314ccd1fcb",
"shasum": ""
},
"require": {
"alibabacloud/credentials": "^1.1",
"php": ">5.5"
},
"type": "library",
"autoload": {
"psr-4": {
"Darabonba\\GatewaySpi\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"Apache-2.0"
],
"authors": [
{
"name": "Alibaba Cloud SDK",
"email": "sdk-team@alibabacloud.com"
}
],
"description": "Alibaba Cloud Gateway SPI Client",
"support": {
"source": "https://github.com/alibabacloud-sdk-php/alibabacloud-gateway-spi/tree/1.0.0"
},
"time": "2022-07-14T05:31:35+00:00"
},
{
"name": "alibabacloud/openapi-core",
"version": "1.0.9",
"source": {
"type": "git",
"url": "https://github.com/alibabacloud-sdk-php/openapi-core.git",
"reference": "7b241b2a0e71f70629e2ecdc5cbe8c8b6b3a7567"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/alibabacloud-sdk-php/openapi-core/zipball/7b241b2a0e71f70629e2ecdc5cbe8c8b6b3a7567",
"reference": "7b241b2a0e71f70629e2ecdc5cbe8c8b6b3a7567",
"shasum": ""
},
"require": {
"alibabacloud/credentials": "^1.2.2",
"alibabacloud/darabonba": "^1",
"alibabacloud/gateway-spi": "^1",
"php": ">5.5"
},
"require-dev": {
"phpunit/phpunit": "^4.8.35|^5.4.3|^9.3",
"symfony/dotenv": "^3.4",
"symfony/var-dumper": "^3.4"
},
"type": "library",
"autoload": {
"psr-4": {
"Darabonba\\OpenApi\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"Apache-2.0"
],
"authors": [
{
"name": "Alibaba Cloud SDK",
"email": "sdk-team@alibabacloud.com"
}
],
"description": "Alibaba Cloud OpenApi Client Core",
"support": {
"issues": "https://github.com/alibabacloud-sdk-php/openapi-core/issues",
"source": "https://github.com/alibabacloud-sdk-php/openapi-core/tree/1.0.9"
},
"time": "2026-01-15T06:47:29+00:00"
},
{
"name": "alibabacloud/tea",
"version": "3.2.1",
@@ -6011,7 +6286,7 @@
],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"stability-flags": {},
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
@@ -6023,6 +6298,6 @@
"ext-openssl": "*",
"ext-swoole": "*"
},
"platform-dev": [],
"plugin-api-version": "2.3.0"
"platform-dev": {},
"plugin-api-version": "2.9.0"
}

View File

@@ -23,5 +23,6 @@ return [
'clear:cache' => \app\command\ClearCache::class,
'reset:password' => \app\command\ResetAdminPwd::class,
'holiday_gift_push_task' => \app\command\HolidayGiftPushTask::class,
'product:spu:backfill' => \app\command\BackfillProductSpu::class,
],
];

View File

@@ -15,6 +15,7 @@ use app\services\product\sku\StoreProductAttrValueServices;
use crmeb\exceptions\AdminException;
use crmeb\services\erp\AccessToken;
use crmeb\services\erp\storage\Jushuitan;
use think\facade\Log;
class Stock
{
@@ -39,7 +40,7 @@ class Stock
/**
* 同步商品库存
* @param array $ids
* @param string $ids
* @return void
* @throws \Exception
*/
@@ -115,6 +116,8 @@ class Stock
$params = $this->getParams($biz);
//请求平台接口
$skuIdsList = array_values(array_filter(array_map('trim', explode(',', trim($codeStr, ", \t\n\r\0\x0B"))), static fn($v) => $v !== ''));
Log::info('jst.api.stock.inventoryQuery.request', ['url' => $url, 'sku_ids_count' => count($skuIdsList), 'sku_ids' => $skuIdsList, 'sku_ids_raw' => $codeStr]);
$request = $this->postRequest($url, $params);
return $request["data"];
}

View File

@@ -14,7 +14,8 @@ namespace crmeb\services\sms\storage;
use AlibabaCloud\SDK\Dysmsapi\V20170525\Dysmsapi;
use AlibabaCloud\SDK\Dysmsapi\V20170525\Models\SendSmsRequest;
use AlibabaCloud\Tea\Exception\TeaError;
use AlibabaCloud\Tea\Utils\Utils\RuntimeOptions;
// use AlibabaCloud\Tea\Utils\Utils\RuntimeOptions;
use AlibabaCloud\Dara\Models\RuntimeOptions;
use crmeb\basic\BaseSmss;
use crmeb\exceptions\ApiException;
use Darabonba\OpenApi\Models\Config as AliConfig;
@@ -33,6 +34,39 @@ class Aliyun extends BaseSmss
protected $AccessKeySecret = '';
protected $SignName = '';
private function ensureAliyunSdkAutoload(): void
{
static $inited = false;
if ($inited) {
return;
}
$inited = true;
$rootDir = dirname(__DIR__, 4) . DIRECTORY_SEPARATOR;
$map = [
'AlibabaCloud\\Tea\\Utils\\' => $rootDir . 'vendor' . DIRECTORY_SEPARATOR . 'alibabacloud' . DIRECTORY_SEPARATOR . 'tea-utils' . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR,
'Darabonba\\OpenApi\\' => $rootDir . 'vendor' . DIRECTORY_SEPARATOR . 'alibabacloud' . DIRECTORY_SEPARATOR . 'darabonba-openapi' . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR,
'AlibabaCloud\\SDK\\Dysmsapi\\V20170525\\' => $rootDir . 'vendor' . DIRECTORY_SEPARATOR . 'alibabacloud' . DIRECTORY_SEPARATOR . 'dysmsapi-20170525' . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR,
];
foreach ($map as $prefix => $baseDir) {
if (!is_dir($baseDir)) {
continue;
}
spl_autoload_register(static function ($class) use ($prefix, $baseDir) {
$len = strlen($prefix);
if (strncmp($class, $prefix, $len) !== 0) {
return;
}
$relative = substr($class, $len);
$file = $baseDir . str_replace('\\', DIRECTORY_SEPARATOR, $relative) . '.php';
if (is_file($file)) {
require_once $file;
}
});
}
}
/**
* @param array $config
* @return mixed|void
@@ -57,6 +91,7 @@ class Aliyun extends BaseSmss
if (empty($phone)) {
return $this->setError('电话号码不能为空');
}
$this->ensureAliyunSdkAutoload();
$config = new AliConfig([
"accessKeyId" => $this->AccessKeyId,
"accessKeySecret" => $this->AccessKeySecret

View File

@@ -71,7 +71,15 @@ class Cron
*/
protected function runInSandbox(callable $callable)
{
try {
$callable();
} catch (\Throwable $e) {
Log::error('cron.sandbox.error ' . json_encode([
'message' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
], JSON_UNESCAPED_UNICODE));
}
}
/**

62
init_jst_token.php Normal file
View File

@@ -0,0 +1,62 @@
#!/usr/bin/env php
<?php
namespace think;
require __DIR__ . '/vendor/autoload.php';
$app = new App();
$app->initialize();
echo "========================================\n";
echo "初始化聚水潭Token文件\n";
echo "========================================\n\n";
$tokenFile = __DIR__ . '/runtime/jstdata/jst_token_data.json';
$dir = dirname($tokenFile);
if (!is_dir($dir)) {
mkdir($dir, 0777, true);
echo "✅ 创建目录: {$dir}\n\n";
}
$accessToken = env('JST_ACCESS_TOKEN', '');
$refreshToken = env('JST_REFRESH_TOKEN', '');
$expiresIn = env('JST_EXPIRES_IN', 2592000);
if (empty($accessToken)) {
echo "❌ .env中的JST_ACCESS_TOKEN为空\n";
echo "请先在.env中配置有效的JST_ACCESS_TOKEN\n\n";
exit(1);
}
$tokenInfo = [
'access_token' => $accessToken,
'expires_in' => (int)$expiresIn,
'refresh_token' => $refreshToken,
'time' => time(),
];
$json = json_encode($tokenInfo, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
file_put_contents($tokenFile, $json);
echo "✅ Token文件已创建: {$tokenFile}\n\n";
echo "文件内容:\n";
echo $json . "\n\n";
echo "字段说明:\n";
echo " - access_token: 访问令牌\n";
echo " - expires_in: 有效期(秒),默认30天\n";
echo " - refresh_token: 刷新令牌(用于自动续期)\n";
echo " - time: 创建时间戳\n\n";
if (empty($refreshToken)) {
echo "⚠️ 警告: refresh_token为空,将无法自动刷新Token\n";
echo "建议在.env中添加:\n";
echo "JST_REFRESH_TOKEN=你的刷新令牌\n\n";
}
echo "========================================\n";
echo "初始化完成!\n";
echo "========================================\n";

View File

@@ -57521,5 +57521,3 @@ INSERT INTO `eb_city_area` (`id`, `path`, `parent_id`, `type`, `name`, `level`,
(44705, '/30722/30792/44689/', 44689, 'street', '新州镇', 4, '460400116000', 0, '2023-11-02 04:28:19'),
(44706, '/30722/30792/44689/', 44689, 'street', '洋浦经济开发区', 4, '460400499000', 0, '2023-11-02 04:28:19'),
(44707, '/30722/30792/44689/', 44689, 'street', '华南热作学院', 4, '460400500000', 0, '2023-11-02 04:28:19');

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
<view class="wrapper data-v-51ea96af" style="{{(colorStyle)}}"><view class="bag data-v-51ea96af"><image src="{{imgHost+'/statics/images/users/login-bg.jpg'}}" alt srcset class="_img data-v-51ea96af"></image></view><view class="system-height data-v-51ea96af" style="{{'height:'+(statusBarHeight)+';'}}"></view><view class="title-bar data-v-51ea96af" style="height:43px;"><block wx:if="{{!isHome}}"><view data-event-opts="{{[['tap',[['back',['$event']]]]]}}" class="icon data-v-51ea96af" bindtap="__e"><image src="../static/left.png" class="data-v-51ea96af"></image></view></block><block wx:else><view data-event-opts="{{[['tap',[['home',['$event']]]]]}}" class="icon data-v-51ea96af" bindtap="__e"><image src="../static/home.png" class="data-v-51ea96af"></image></view></block>{{''+pageTitle+''}}</view><view class="page-msg data-v-51ea96af"><view class="title data-v-51ea96af">{{''+(pageType==1?'绑定手机号':'手机号登录')+''}}</view><view class="tip data-v-51ea96af">{{''+(pageType==1?'登录注册需绑定手机号':'首次登录会自动注册')+''}}</view></view><view class="page-form data-v-51ea96af"><view class="item data-v-51ea96af"><input type="number" placeholder="填写手机号码" placeholder-class="placeholder" maxlength="{{11}}" data-event-opts="{{[['input',[['__set_model',['','phone','$event',[]]]]]]}}" value="{{phone}}" bindinput="__e" class="data-v-51ea96af"/></view><view class="item acea-row row-between-wrapper data-v-51ea96af"><input class="codeIput data-v-51ea96af" type="number" placeholder="填写验证码" placeholder-class="placeholder" maxlength="{{6}}" data-event-opts="{{[['input',[['__set_model',['','captcha','$event',[]]]]]]}}" value="{{captcha}}" bindinput="__e"/><view class="line data-v-51ea96af"></view><button class="{{['code','font-num','data-v-51ea96af',disabled===true?'on':'']}}" disabled="{{disabled}}" data-event-opts="{{[['tap',[['code',['$event']]]]]}}" bindtap="__e">{{''+text+''}}</button></view><view data-event-opts="{{[['tap',[['submitData',['$event']]]]]}}" class="btn data-v-51ea96af" bindtap="__e">{{''+(pageType==1?'绑定手机号':'立即登录')+''}}</view></view><view class="protocol data-v-51ea96af"><checkbox-group data-event-opts="{{[['tap',[['ChangeIsDefault',['$event']]]]]}}" catchtap="__e" class="data-v-51ea96af"><checkbox class="{{['data-v-51ea96af',inAnimation?'trembling':'']}}" color="#ffffff" backgroundColor="#ffffff" activeBackgroundColor="var(--view-theme)" activeBorderColor="var(--view-theme)" checked="{{protocol?true:false}}" data-event-opts="{{[['animationend',[['e0',['$event']]]]]}}" bindanimationend="__e"></checkbox><text data-event-opts="{{[['tap',[['ChangeIsDefault',['$event']]]]]}}" catchtap="__e" class="data-v-51ea96af">已阅读并同意</text><text data-event-opts="{{[['tap',[['privacy',['user']]]]]}}" class="main-color data-v-51ea96af" catchtap="__e">《用户协议》</text> 与<text data-event-opts="{{[['tap',[['privacy',['privacy']]]]]}}" class="main-color data-v-51ea96af" catchtap="__e">《隐私协议》</text></checkbox-group></view><verify vue-id="2587305f-1" captchaType="{{captchaType}}" imgSize="{{({width:'330px',height:'155px'})}}" data-ref="verify" data-event-opts="{{[['^success',[['success']]]]}}" bind:success="__e" class="data-v-51ea96af vue-ref" bind:__l="__l"></verify><edit-user-modal vue-id="2587305f-2" isShow="{{isShow}}" data-event-opts="{{[['^closeEdit',[['closeEdit']]],['^editSuccess',[['editSuccess']]]]}}" bind:closeEdit="__e" bind:editSuccess="__e" class="data-v-51ea96af" bind:__l="__l"></edit-user-modal></view>
<view class="wrapper data-v-51ea96af" style="{{(colorStyle)}}"><view class="bag data-v-51ea96af"><image src="{{imgHost+'/statics/images/users/login-bg.jpg'}}" alt srcset class="_img data-v-51ea96af"></image></view><view class="system-height data-v-51ea96af" style="{{'height:'+(statusBarHeight)+';'}}"></view><view class="title-bar data-v-51ea96af" style="height:43px;"><block wx:if="{{!isHome}}"><view data-event-opts="{{[['tap',[['back',['$event']]]]]}}" class="icon data-v-51ea96af" bindtap="__e"><image src="../static/left.png" class="data-v-51ea96af"></image></view></block><block wx:else><view data-event-opts="{{[['tap',[['home',['$event']]]]]}}" class="icon data-v-51ea96af" bindtap="__e"><image src="../static/home.png" class="data-v-51ea96af"></image></view></block>{{''+pageTitle+''}}</view><view class="page-msg data-v-51ea96af"><view class="title data-v-51ea96af">{{''+(pageType==1?'绑定手机号':'手机号登录')+''}}</view><view class="tip data-v-51ea96af">{{''+(pageType==1?'登录注册需绑定手机号':'首次登录会自动注册')+''}}</view></view><view class="page-form data-v-51ea96af"><view class="item data-v-51ea96af"><input type="number" placeholder="填写手机号码" placeholder-class="placeholder" maxlength="{{11}}" data-event-opts="{{[['input',[['__set_model',['','phone','$event',[]]]]]]}}" value="{{phone}}" bindinput="__e" class="data-v-51ea96af"/></view><view class="item acea-row row-between-wrapper data-v-51ea96af"><input class="codeIput data-v-51ea96af" type="number" placeholder="填写验证码" placeholder-class="placeholder" maxlength="{{4}}" data-event-opts="{{[['input',[['__set_model',['','captcha','$event',[]]]]]]}}" value="{{captcha}}" bindinput="__e"/><view class="line data-v-51ea96af"></view><button class="{{['code','font-num','data-v-51ea96af',disabled===true?'on':'']}}" disabled="{{disabled}}" data-event-opts="{{[['tap',[['code',['$event']]]]]}}" bindtap="__e">{{''+text+''}}</button></view><view data-event-opts="{{[['tap',[['submitData',['$event']]]]]}}" class="btn data-v-51ea96af" bindtap="__e">{{''+(pageType==1?'绑定手机号':'立即登录')+''}}</view></view><view class="protocol data-v-51ea96af"><checkbox-group data-event-opts="{{[['tap',[['ChangeIsDefault',['$event']]]]]}}" catchtap="__e" class="data-v-51ea96af"><checkbox class="{{['data-v-51ea96af',inAnimation?'trembling':'']}}" color="#ffffff" backgroundColor="#ffffff" activeBackgroundColor="var(--view-theme)" activeBorderColor="var(--view-theme)" checked="{{protocol?true:false}}" data-event-opts="{{[['animationend',[['e0',['$event']]]]]}}" bindanimationend="__e"></checkbox><text data-event-opts="{{[['tap',[['ChangeIsDefault',['$event']]]]]}}" catchtap="__e" class="data-v-51ea96af">已阅读并同意</text><text data-event-opts="{{[['tap',[['privacy',['user']]]]]}}" class="main-color data-v-51ea96af" catchtap="__e">《用户协议》</text> 与<text data-event-opts="{{[['tap',[['privacy',['privacy']]]]]}}" class="main-color data-v-51ea96af" catchtap="__e">《隐私协议》</text></checkbox-group></view><verify vue-id="2587305f-1" captchaType="{{captchaType}}" imgSize="{{({width:'330px',height:'155px'})}}" data-ref="verify" data-event-opts="{{[['^success',[['success']]]]}}" bind:success="__e" class="data-v-51ea96af vue-ref" bind:__l="__l"></verify><edit-user-modal vue-id="2587305f-2" isShow="{{isShow}}" data-event-opts="{{[['^closeEdit',[['closeEdit']]],['^editSuccess',[['editSuccess']]]]}}" bind:closeEdit="__e" bind:editSuccess="__e" class="data-v-51ea96af" bind:__l="__l"></edit-user-modal></view>

View File

@@ -1 +1 @@
<view class="warp data-v-4bc8dffd"><view class="w-full fixed-lt z-20 data-v-4bc8dffd" style="{{'padding-top:'+(sysHeight+'px')+';'}}"><view class="h-80 px-20 flex-between-center data-v-4bc8dffd"><text data-event-opts="{{[['tap',[['pageBack',['$event']]]]]}}" class="iconfont icon-ic_leftarrow fs-40 text--w111-000 data-v-4bc8dffd" bindtap="__e"></text></view></view><view class="header-top relative data-v-4bc8dffd"><image src="{{headerBg}}" mode class="data-v-4bc8dffd"></image></view><view class="bg-v-gradient pl-20 pr-20 pb-24 relative data-v-4bc8dffd"><view class="bg--w111-fff rd-24rpx content-box data-v-4bc8dffd"><view class="fs-30 fw-500 lh-42rpx data-v-4bc8dffd">请填写以下信息</view><view class="cell flex-between-center mt-64 data-v-4bc8dffd"><view class="fs-28 lh-40rpx data-v-4bc8dffd">用户昵称</view><view class="fs-28 text-right data-v-4bc8dffd">{{form.nickname}}</view></view><view class="cell flex-between-center mt-64 data-v-4bc8dffd"><view class="fs-28 lh-40rpx data-v-4bc8dffd">用户ID</view><view class="fs-28 text-right data-v-4bc8dffd">{{form.uid}}</view></view><view class="cell flex-between-center mt-64 data-v-4bc8dffd"><view class="fs-28 lh-40rpx data-v-4bc8dffd">分销员姓名</view><input class="fs-28 text-right data-v-4bc8dffd" type="text" placeholder="请输入分销员姓名" placeholder-class="text--w111-ccc" data-event-opts="{{[['input',[['__set_model',['$0','real_name','$event',[]],['form']]]]]}}" value="{{form.real_name}}" bindinput="__e"/></view><view class="cell flex-between-center mt-64 data-v-4bc8dffd"><view class="fs-28 lh-40rpx data-v-4bc8dffd">手机号</view><input class="fs-28 text-right data-v-4bc8dffd" type="number" placeholder="请输入手机号" placeholder-class="text--w111-ccc" data-event-opts="{{[['input',[['__set_model',['$0','phone','$event',[]],['form']]]]]}}" value="{{form.phone}}" bindinput="__e"/></view><block wx:if="{{brokerageApplyPhoneVerify}}"><view class="cell flex-between-center mt-64 data-v-4bc8dffd"><view class="fs-28 lh-40rpx data-v-4bc8dffd">验证码</view><view class="flex-y-center data-v-4bc8dffd"><input class="fs-28 text-right data-v-4bc8dffd" type="number" placeholder="请输入验证码" placeholder-class="text--w111-ccc" data-event-opts="{{[['input',[['__set_model',['$0','code','$event',[]],['form']]]]]}}" value="{{form.code}}" bindinput="__e"/><button class="code-btn w-168 h-56 flex-center fs-24 ml-20 data-v-4bc8dffd" disabled="{{disabled}}" data-event-opts="{{[['tap',[['code',['$event']]]]]}}" bindtap="__e">{{text}}</button></view></view></block><view class="cell mt-64 data-v-4bc8dffd"><view class="fs-28 lh-40rpx data-v-4bc8dffd">申请说明</view><view class="text--w111-ccc mt-12 data-v-4bc8dffd">{{''+brokerage_apply_explain+''}}</view></view><view class="flex-y-center mt-100rpx mb-24 data-v-4bc8dffd"><text data-event-opts="{{[['tap',[['proviceSelect',['$event']]]]]}}" class="{{['iconfont','fs-30','data-v-4bc8dffd',isSelect?'icon-a-ic_CompleteSelect':'icon-ic_unselect']}}" bindtap="__e"></text><text class="fs-24 text--w111-999 pl-12 data-v-4bc8dffd">已阅读并同意</text><text data-event-opts="{{[['tap',[['getAgreement',['$event']]]]]}}" class="fs-24 font-red data-v-4bc8dffd" bindtap="__e">《分销说明》</text></view><view data-event-opts="{{[['tap',[['submitSupply',['$event']]]]]}}" class="{{['w-full','h-88','rd-44rpx','flex-center','text--w111-fff','fs-28','data-v-4bc8dffd',isSelectStar?'bg-red':'bg-disabled']}}" bindtap="__e">提交申请</view></view></view><verify vue-id="15486aa0-1" captchaType="{{captchaType}}" imgSize="{{({width:'330px',height:'155px'})}}" data-ref="verify" data-event-opts="{{[['^success',[['success']]]]}}" bind:success="__e" class="data-v-4bc8dffd vue-ref" bind:__l="__l"></verify><tui-modal vue-id="15486aa0-2" show="{{showModal}}" maskClosable="{{true}}" custom="{{true}}" data-event-opts="{{[['^cancel',[['hideModal']]]]}}" bind:cancel="__e" class="data-v-4bc8dffd" bind:__l="__l" vue-slots="{{['default']}}"><view class="tui-modal-custom data-v-4bc8dffd"><view class="fs-32 fw-500 lh-44rpx text-center data-v-4bc8dffd">分销说明</view><view class="fs-28 text--w111-666 lh-44rpx mt-24 desc-box data-v-4bc8dffd"><rich-text nodes="{{supplierAgreement}}" class="data-v-4bc8dffd"></rich-text></view><view data-event-opts="{{[['tap',[['hideModal',['$event']]]]]}}" class="w-full h-72 rd-36rpx flex-center bg-red fs-26 text--w111-fff mt-32 data-v-4bc8dffd" bindtap="__e">知道了</view></view></tui-modal></view>
<view class="warp data-v-4bc8dffd"><view class="w-full fixed-lt z-20 data-v-4bc8dffd" style="{{'padding-top:'+(sysHeight+'px')+';'}}"><view class="h-80 px-20 flex-between-center data-v-4bc8dffd"><text data-event-opts="{{[['tap',[['pageBack',['$event']]]]]}}" class="iconfont icon-ic_leftarrow fs-40 text--w111-000 data-v-4bc8dffd" bindtap="__e"></text></view></view><view class="header-top relative data-v-4bc8dffd"><image src="{{headerBg}}" mode class="data-v-4bc8dffd"></image></view><view class="bg-v-gradient pl-20 pr-20 pb-24 relative data-v-4bc8dffd"><view class="bg--w111-fff rd-24rpx content-box data-v-4bc8dffd"><view class="fs-30 fw-500 lh-42rpx data-v-4bc8dffd">请填写以下信息</view><view class="cell flex-between-center mt-64 data-v-4bc8dffd"><view class="fs-28 lh-40rpx data-v-4bc8dffd">用户昵称</view><view class="fs-28 text-right data-v-4bc8dffd">{{form.nickname}}</view></view><view class="cell flex-between-center mt-64 data-v-4bc8dffd"><view class="fs-28 lh-40rpx data-v-4bc8dffd">用户ID</view><view class="fs-28 text-right data-v-4bc8dffd">{{form.uid}}</view></view><view class="cell flex-between-center mt-64 data-v-4bc8dffd"><view class="fs-28 lh-40rpx data-v-4bc8dffd">分销员姓名</view><input class="fs-28 text-right data-v-4bc8dffd" type="text" placeholder="请输入分销员姓名" placeholder-class="text--w111-ccc" data-event-opts="{{[['input',[['__set_model',['$0','real_name','$event',[]],['form']]]]]}}" value="{{form.real_name}}" bindinput="__e"/></view><view class="cell flex-between-center mt-64 data-v-4bc8dffd"><view class="fs-28 lh-40rpx data-v-4bc8dffd">手机号</view><input class="fs-28 text-right data-v-4bc8dffd" type="number" placeholder="请输入手机号" placeholder-class="text--w111-ccc" data-event-opts="{{[['input',[['__set_model',['$0','phone','$event',[]],['form']]]]]}}" value="{{form.phone}}" bindinput="__e"/></view><block wx:if="{{brokerageApplyPhoneVerify}}"><view class="cell flex-between-center mt-64 data-v-4bc8dffd"><view class="fs-28 lh-40rpx data-v-4bc8dffd">验证码</view><view class="flex-y-center data-v-4bc8dffd"><input class="fs-28 text-right data-v-4bc8dffd" type="number" placeholder="请输入验证码" placeholder-class="text--w111-ccc" maxlength="{{4}}" data-event-opts="{{[['input',[['__set_model',['$0','code','$event',[]],['form']]]]]}}" value="{{form.code}}" bindinput="__e"/><button class="code-btn w-168 h-56 flex-center fs-24 ml-20 data-v-4bc8dffd" disabled="{{disabled}}" data-event-opts="{{[['tap',[['code',['$event']]]]]}}" bindtap="__e">{{text}}</button></view></view></block><view class="cell mt-64 data-v-4bc8dffd"><view class="fs-28 lh-40rpx data-v-4bc8dffd">申请说明</view><view class="text--w111-ccc mt-12 data-v-4bc8dffd">{{''+brokerage_apply_explain+''}}</view></view><view class="flex-y-center mt-100rpx mb-24 data-v-4bc8dffd"><text data-event-opts="{{[['tap',[['proviceSelect',['$event']]]]]}}" class="{{['iconfont','fs-30','data-v-4bc8dffd',isSelect?'icon-a-ic_CompleteSelect':'icon-ic_unselect']}}" bindtap="__e"></text><text class="fs-24 text--w111-999 pl-12 data-v-4bc8dffd">已阅读并同意</text><text data-event-opts="{{[['tap',[['getAgreement',['$event']]]]]}}" class="fs-24 font-red data-v-4bc8dffd" bindtap="__e">《分销说明》</text></view><view data-event-opts="{{[['tap',[['submitSupply',['$event']]]]]}}" class="{{['w-full','h-88','rd-44rpx','flex-center','text--w111-fff','fs-28','data-v-4bc8dffd',isSelectStar?'bg-red':'bg-disabled']}}" bindtap="__e">提交申请</view></view></view><verify vue-id="15486aa0-1" captchaType="{{captchaType}}" imgSize="{{({width:'330px',height:'155px'})}}" data-ref="verify" data-event-opts="{{[['^success',[['success']]]]}}" bind:success="__e" class="data-v-4bc8dffd vue-ref" bind:__l="__l"></verify><tui-modal vue-id="15486aa0-2" show="{{showModal}}" maskClosable="{{true}}" custom="{{true}}" data-event-opts="{{[['^cancel',[['hideModal']]]]}}" bind:cancel="__e" class="data-v-4bc8dffd" bind:__l="__l" vue-slots="{{['default']}}"><view class="tui-modal-custom data-v-4bc8dffd"><view class="fs-32 fw-500 lh-44rpx text-center data-v-4bc8dffd">分销说明</view><view class="fs-28 text--w111-666 lh-44rpx mt-24 desc-box data-v-4bc8dffd"><rich-text nodes="{{supplierAgreement}}" class="data-v-4bc8dffd"></rich-text></view><view data-event-opts="{{[['tap',[['hideModal',['$event']]]]]}}" class="w-full h-72 rd-36rpx flex-center bg-red fs-26 text--w111-fff mt-32 data-v-4bc8dffd" bindtap="__e">知道了</view></view></tui-modal></view>

View File

@@ -1 +1 @@
<view class="wrapper data-v-32ac9622" style="{{(colorStyle)}}"><view class="bag data-v-32ac9622"><image src="{{imgHost+'/statics/images/users/login-bg.jpg'}}" alt srcset class="_img data-v-32ac9622"></image></view><view class="title-bar data-v-32ac9622" style="height:43px;"><block wx:if="{{!isHome}}"><view data-event-opts="{{[['tap',[['back',['$event']]]]]}}" class="icon data-v-32ac9622" bindtap="__e"><image src="../static/left.png" class="data-v-32ac9622"></image></view></block><block wx:else><view data-event-opts="{{[['tap',[['home',['$event']]]]]}}" class="icon data-v-32ac9622" bindtap="__e"><image src="../static/home.png" class="data-v-32ac9622"></image></view></block>{{''+pageTitle+''}}</view><view class="page-msg data-v-32ac9622"><view class="title data-v-32ac9622">{{''+(current?'快速登录':'账号登录')+''}}</view><view class="tip data-v-32ac9622"> 首次登录会自动注册 </view></view><view class="page-form data-v-32ac9622"><view class="item data-v-32ac9622"><input type="number" placeholder="请输入手机号码" placeholder-class="placeholder" maxlength="{{11}}" adjust-position="{{false}}" data-event-opts="{{[['input',[['__set_model',['','account','$event',[]]]]]]}}" value="{{account}}" bindinput="__e" class="data-v-32ac9622"/></view><block wx:if="{{!current}}"><view class="item acea-row row-between-wrapper data-v-32ac9622"><input class="codeIput data-v-32ac9622" type="password" placeholder="请输入密码" placeholder-class="placeholder" adjust-position="{{false}}" data-event-opts="{{[['input',[['__set_model',['','password','$event',[]]]]]]}}" value="{{password}}" bindinput="__e"/><view class="line data-v-32ac9622"></view><navigator class="code font-num data-v-32ac9622" hover-class="none" url="/pages/users/retrievePassword/index"> 忘记密码 </navigator></view></block><block wx:else><view class="item acea-row row-between-wrapper data-v-32ac9622"><input class="codeIput data-v-32ac9622" type="number" placeholder="请输入验证码" placeholder-class="placeholder" maxlength="{{6}}" data-event-opts="{{[['input',[['__set_model',['','captcha','$event',[]]]]]]}}" value="{{captcha}}" bindinput="__e"/><view class="line data-v-32ac9622"></view><button class="{{['code','font-num','data-v-32ac9622',disabled===true?'on':'']}}" disabled="{{disabled}}" data-event-opts="{{[['tap',[['code',['$event']]]]]}}" bindtap="__e">{{''+text+''}}</button></view></block><view data-event-opts="{{[['tap',[['submitData',['$event']]]]]}}" class="btn data-v-32ac9622" bindtap="__e"> 立即登录 </view><view data-event-opts="{{[['tap',[['e0',['$event']]]]]}}" class="text-center fs-32 text--w111-999 mt-32 data-v-32ac9622" bindtap="__e">{{current?'账号登录':'手机号登录'}}</view></view><view class="protocol data-v-32ac9622"><checkbox-group data-event-opts="{{[['tap',[['ChangeIsDefault',['$event']]]]]}}" catchtap="__e" class="data-v-32ac9622"><checkbox class="{{['data-v-32ac9622',inAnimation?'trembling':'']}}" checked="{{protocol?true:false}}" color="#ffffff" backgroundColor="#ffffff" activeBackgroundColor="var(--view-theme)" activeBorderColor="var(--view-theme)" data-event-opts="{{[['animationend',[['e1',['$event']]]]]}}" bindanimationend="__e"></checkbox><text data-event-opts="{{[['tap',[['ChangeIsDefault',['$event']]]]]}}" catchtap="__e" class="data-v-32ac9622">已阅读并同意</text><text data-event-opts="{{[['tap',[['privacy',['user']]]]]}}" class="main-color data-v-32ac9622" catchtap="__e">《用户协议》</text> 与<text data-event-opts="{{[['tap',[['privacy',['privacy']]]]]}}" class="main-color data-v-32ac9622" catchtap="__e">《隐私协议》</text></checkbox-group></view><block wx:if="{{!disabled}}"><verify vue-id="220b5d0a-1" captchaType="{{captchaType}}" imgSize="{{({width:'330px',height:'155px'})}}" data-ref="verify" data-event-opts="{{[['^success',[['success']]]]}}" bind:success="__e" class="data-v-32ac9622 vue-ref" bind:__l="__l"></verify></block></view>
<view class="wrapper data-v-32ac9622" style="{{(colorStyle)}}"><view class="bag data-v-32ac9622"><image src="{{imgHost+'/statics/images/users/login-bg.jpg'}}" alt srcset class="_img data-v-32ac9622"></image></view><view class="title-bar data-v-32ac9622" style="height:43px;"><block wx:if="{{!isHome}}"><view data-event-opts="{{[['tap',[['back',['$event']]]]]}}" class="icon data-v-32ac9622" bindtap="__e"><image src="../static/left.png" class="data-v-32ac9622"></image></view></block><block wx:else><view data-event-opts="{{[['tap',[['home',['$event']]]]]}}" class="icon data-v-32ac9622" bindtap="__e"><image src="../static/home.png" class="data-v-32ac9622"></image></view></block>{{''+pageTitle+''}}</view><view class="page-msg data-v-32ac9622"><view class="title data-v-32ac9622">{{''+(current?'快速登录':'账号登录')+''}}</view><view class="tip data-v-32ac9622"> 首次登录会自动注册 </view></view><view class="page-form data-v-32ac9622"><view class="item data-v-32ac9622"><input type="number" placeholder="请输入手机号码" placeholder-class="placeholder" maxlength="{{11}}" adjust-position="{{false}}" data-event-opts="{{[['input',[['__set_model',['','account','$event',[]]]]]]}}" value="{{account}}" bindinput="__e" class="data-v-32ac9622"/></view><block wx:if="{{!current}}"><view class="item acea-row row-between-wrapper data-v-32ac9622"><input class="codeIput data-v-32ac9622" type="password" placeholder="请输入密码" placeholder-class="placeholder" adjust-position="{{false}}" data-event-opts="{{[['input',[['__set_model',['','password','$event',[]]]]]]}}" value="{{password}}" bindinput="__e"/><view class="line data-v-32ac9622"></view><navigator class="code font-num data-v-32ac9622" hover-class="none" url="/pages/users/retrievePassword/index"> 忘记密码 </navigator></view></block><block wx:else><view class="item acea-row row-between-wrapper data-v-32ac9622"><input class="codeIput data-v-32ac9622" type="number" placeholder="请输入验证码" placeholder-class="placeholder" maxlength="{{4}}" data-event-opts="{{[['input',[['__set_model',['','captcha','$event',[]]]]]]}}" value="{{captcha}}" bindinput="__e"/><view class="line data-v-32ac9622"></view><button class="{{['code','font-num','data-v-32ac9622',disabled===true?'on':'']}}" disabled="{{disabled}}" data-event-opts="{{[['tap',[['code',['$event']]]]]}}" bindtap="__e">{{''+text+''}}</button></view></block><view data-event-opts="{{[['tap',[['submitData',['$event']]]]]}}" class="btn data-v-32ac9622" bindtap="__e"> 立即登录 </view><view data-event-opts="{{[['tap',[['e0',['$event']]]]]}}" class="text-center fs-32 text--w111-999 mt-32 data-v-32ac9622" bindtap="__e">{{current?'账号登录':'手机号登录'}}</view></view><view class="protocol data-v-32ac9622"><checkbox-group data-event-opts="{{[['tap',[['ChangeIsDefault',['$event']]]]]}}" catchtap="__e" class="data-v-32ac9622"><checkbox class="{{['data-v-32ac9622',inAnimation?'trembling':'']}}" checked="{{protocol?true:false}}" color="#ffffff" backgroundColor="#ffffff" activeBackgroundColor="var(--view-theme)" activeBorderColor="var(--view-theme)" data-event-opts="{{[['animationend',[['e1',['$event']]]]]}}" bindanimationend="__e"></checkbox><text data-event-opts="{{[['tap',[['ChangeIsDefault',['$event']]]]]}}" catchtap="__e" class="data-v-32ac9622">已阅读并同意</text><text data-event-opts="{{[['tap',[['privacy',['user']]]]]}}" class="main-color data-v-32ac9622" catchtap="__e">《用户协议》</text> 与<text data-event-opts="{{[['tap',[['privacy',['privacy']]]]]}}" class="main-color data-v-32ac9622" catchtap="__e">《隐私协议》</text></checkbox-group></view><block wx:if="{{!disabled}}"><verify vue-id="220b5d0a-1" captchaType="{{captchaType}}" imgSize="{{({width:'330px',height:'155px'})}}" data-ref="verify" data-event-opts="{{[['^success',[['success']]]]}}" bind:success="__e" class="data-v-32ac9622 vue-ref" bind:__l="__l"></verify></block></view>

View File

@@ -1 +1 @@
<view class="wrapper data-v-1873fff7" style="{{(colorStyle)}}"><view class="bag data-v-1873fff7"><image src="{{imgHost+'/statics/images/users/login-bg.jpg'}}" alt srcset class="_img data-v-1873fff7"></image></view><view class="title-bar data-v-1873fff7" style="height:43px;"><block wx:if="{{!isHome}}"><view data-event-opts="{{[['tap',[['back',['$event']]]]]}}" class="icon data-v-1873fff7" bindtap="__e"><image src="../static/left.png" class="data-v-1873fff7"></image></view></block><block wx:else><view data-event-opts="{{[['tap',[['home',['$event']]]]]}}" class="icon data-v-1873fff7" bindtap="__e"><image src="../static/home.png" class="data-v-1873fff7"></image></view></block>{{''+pageTitle+''}}</view><view class="page-msg data-v-1873fff7"><view class="title data-v-1873fff7"> 找回密码 </view><view class="tip data-v-1873fff7"></view></view><view class="page-form data-v-1873fff7"><view class="item data-v-1873fff7"><input type="number" placeholder="请输入手机号码" placeholder-class="placeholder" maxlength="{{11}}" data-event-opts="{{[['input',[['__set_model',['','account','$event',[]]]]]]}}" value="{{account}}" bindinput="__e" class="data-v-1873fff7"/></view><view class="item acea-row row-between-wrapper data-v-1873fff7"><input class="codeIput data-v-1873fff7" type="number" placeholder="请输入验证码" placeholder-class="placeholder" maxlength="{{6}}" data-event-opts="{{[['input',[['__set_model',['','captcha','$event',[]]]]]]}}" value="{{captcha}}" bindinput="__e"/><view class="line data-v-1873fff7"></view><button class="{{['code','font-num','data-v-1873fff7',disabled===true?'on':'']}}" disabled="{{disabled}}" data-event-opts="{{[['tap',[['code',['$event']]]]]}}" bindtap="__e">{{''+text+''}}</button></view><view class="item data-v-1873fff7"><input type="password" placeholder="请输入密码" placeholder-class="placeholder" maxlength="{{28}}" data-event-opts="{{[['input',[['__set_model',['','password','$event',[]]]]]]}}" value="{{password}}" bindinput="__e" class="data-v-1873fff7"/></view><view class="item data-v-1873fff7"><input type="password" placeholder="请再次确认密码" placeholder-class="placeholder" maxlength="{{28}}" data-event-opts="{{[['input',[['__set_model',['','passwordAgain','$event',[]]]]]]}}" value="{{passwordAgain}}" bindinput="__e" class="data-v-1873fff7"/></view><view data-event-opts="{{[['tap',[['registerReset',['$event']]]]]}}" class="btn data-v-1873fff7" bindtap="__e"> 确认 </view><view data-event-opts="{{[['tap',[['back',['$event']]]]]}}" class="text-center fs-32 text--w111-999 mt-32 data-v-1873fff7" bindtap="__e">立即登录</view></view><verify vue-id="3a6ffba8-1" captchaType="blockPuzzle" imgSize="{{({width:'330px',height:'155px'})}}" data-ref="verify" data-event-opts="{{[['^success',[['success']]]]}}" bind:success="__e" class="data-v-1873fff7 vue-ref" bind:__l="__l"></verify></view>
<view class="wrapper data-v-1873fff7" style="{{(colorStyle)}}"><view class="bag data-v-1873fff7"><image src="{{imgHost+'/statics/images/users/login-bg.jpg'}}" alt srcset class="_img data-v-1873fff7"></image></view><view class="title-bar data-v-1873fff7" style="height:43px;"><block wx:if="{{!isHome}}"><view data-event-opts="{{[['tap',[['back',['$event']]]]]}}" class="icon data-v-1873fff7" bindtap="__e"><image src="../static/left.png" class="data-v-1873fff7"></image></view></block><block wx:else><view data-event-opts="{{[['tap',[['home',['$event']]]]]}}" class="icon data-v-1873fff7" bindtap="__e"><image src="../static/home.png" class="data-v-1873fff7"></image></view></block>{{''+pageTitle+''}}</view><view class="page-msg data-v-1873fff7"><view class="title data-v-1873fff7"> 找回密码 </view><view class="tip data-v-1873fff7"></view></view><view class="page-form data-v-1873fff7"><view class="item data-v-1873fff7"><input type="number" placeholder="请输入手机号码" placeholder-class="placeholder" maxlength="{{11}}" data-event-opts="{{[['input',[['__set_model',['','account','$event',[]]]]]]}}" value="{{account}}" bindinput="__e" class="data-v-1873fff7"/></view><view class="item acea-row row-between-wrapper data-v-1873fff7"><input class="codeIput data-v-1873fff7" type="number" placeholder="请输入验证码" placeholder-class="placeholder" maxlength="{{4}}" data-event-opts="{{[['input',[['__set_model',['','captcha','$event',[]]]]]]}}" value="{{captcha}}" bindinput="__e"/><view class="line data-v-1873fff7"></view><button class="{{['code','font-num','data-v-1873fff7',disabled===true?'on':'']}}" disabled="{{disabled}}" data-event-opts="{{[['tap',[['code',['$event']]]]]}}" bindtap="__e">{{''+text+''}}</button></view><view class="item data-v-1873fff7"><input type="password" placeholder="请输入密码" placeholder-class="placeholder" maxlength="{{28}}" data-event-opts="{{[['input',[['__set_model',['','password','$event',[]]]]]]}}" value="{{password}}" bindinput="__e" class="data-v-1873fff7"/></view><view class="item data-v-1873fff7"><input type="password" placeholder="请再次确认密码" placeholder-class="placeholder" maxlength="{{28}}" data-event-opts="{{[['input',[['__set_model',['','passwordAgain','$event',[]]]]]]}}" value="{{passwordAgain}}" bindinput="__e" class="data-v-1873fff7"/></view><view data-event-opts="{{[['tap',[['registerReset',['$event']]]]]}}" class="btn data-v-1873fff7" bindtap="__e"> 确认 </view><view data-event-opts="{{[['tap',[['back',['$event']]]]]}}" class="text-center fs-32 text--w111-999 mt-32 data-v-1873fff7" bindtap="__e">立即登录</view></view><verify vue-id="3a6ffba8-1" captchaType="blockPuzzle" imgSize="{{({width:'330px',height:'155px'})}}" data-ref="verify" data-event-opts="{{[['^success',[['success']]]]}}" bind:success="__e" class="data-v-1873fff7 vue-ref" bind:__l="__l"></verify></view>

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
<view style="{{(colorStyle)}}"><form data-event-opts="{{[['submit',[['editPwd',['$event']]]]]}}" bindsubmit="__e"><view class="ChangePassword"><view class="list"><view class="item"><input type="number" placeholder="填写手机号码" placeholder-class="placeholder" data-event-opts="{{[['input',[['__set_model',['','phone','$event',[]]]]]]}}" value="{{phone}}" bindinput="__e"/></view><view class="item acea-row row-between-wrapper"><input class="codeIput" type="number" placeholder="填写验证码" placeholder-class="placeholder" data-event-opts="{{[['input',[['__set_model',['','captcha','$event',[]]]]]]}}" value="{{captcha}}" bindinput="__e"/><button class="{{['code','font-num',disabled===true?'on':'']}}" disabled="{{disabled}}" data-event-opts="{{[['tap',[['code',['$event']]]]]}}" bindtap="__e">{{''+text+''}}</button></view></view><button class="confirmBnt bg-color" form-type="submit">确认绑定</button></view></form><verify class="vue-ref" vue-id="317f116e-1" captchaType="{{captchaType}}" imgSize="{{({width:'330px',height:'155px'})}}" data-ref="verify" data-event-opts="{{[['^success',[['success']]]]}}" bind:success="__e" bind:__l="__l"></verify><block wx:if="{{isShowAuth}}"><authorize bind:authColse="__e" bind:onLoadFun="__e" vue-id="317f116e-2" data-event-opts="{{[['^authColse',[['authColse']]],['^onLoadFun',[['onLoadFun']]]]}}" bind:__l="__l"></authorize></block></view>
<view style="{{(colorStyle)}}"><form data-event-opts="{{[['submit',[['editPwd',['$event']]]]]}}" bindsubmit="__e"><view class="ChangePassword"><view class="list"><view class="item"><input type="number" placeholder="填写手机号码" placeholder-class="placeholder" data-event-opts="{{[['input',[['__set_model',['','phone','$event',[]]]]]]}}" value="{{phone}}" bindinput="__e"/></view><view class="item acea-row row-between-wrapper"><input class="codeIput" type="number" placeholder="填写验证码" placeholder-class="placeholder" maxlength="{{4}}" data-event-opts="{{[['input',[['__set_model',['','captcha','$event',[]]]]]]}}" value="{{captcha}}" bindinput="__e"/><button class="{{['code','font-num',disabled===true?'on':'']}}" disabled="{{disabled}}" data-event-opts="{{[['tap',[['code',['$event']]]]]}}" bindtap="__e">{{''+text+''}}</button></view></view><button class="confirmBnt bg-color" form-type="submit">确认绑定</button></view></form><verify class="vue-ref" vue-id="317f116e-1" captchaType="{{captchaType}}" imgSize="{{({width:'330px',height:'155px'})}}" data-ref="verify" data-event-opts="{{[['^success',[['success']]]]}}" bind:success="__e" bind:__l="__l"></verify><block wx:if="{{isShowAuth}}"><authorize bind:authColse="__e" bind:onLoadFun="__e" vue-id="317f116e-2" data-event-opts="{{[['^authColse',[['authColse']]],['^onLoadFun',[['onLoadFun']]]]}}" bind:__l="__l"></authorize></block></view>

View File

@@ -1 +1 @@
<view style="{{(colorStyle)}}"><view class="ChangePassword"><form data-event-opts="{{[['submit',[['editPwd',['$event']]]]]}}" bindsubmit="__e"><view class="phone">{{"当前手机号:"+phone}}</view><view class="list"><view class="item"><input type="password" placeholder="设置新密码" placeholder-class="placeholder" name="password" value="{{password}}"/></view><view class="item"><input type="password" placeholder="确认新密码" placeholder-class="placeholder" name="qr_password" value="{{qr_password}}"/></view><view class="item acea-row row-between-wrapper"><input class="codeIput" type="number" placeholder="填写验证码" placeholder-class="placeholder" name="captcha" value="{{captcha}}"/><button class="{{['code','font-num',disabled===true?'on':'']}}" disabled="{{disabled}}" data-event-opts="{{[['tap',[['code',['$event']]]]]}}" bindtap="__e">{{''+text+''}}</button></view></view><button class="confirmBnt bg-color" form-type="submit">确认修改</button></form></view><verify class="vue-ref" vue-id="2ec06c53-1" captchaType="{{captchaType}}" imgSize="{{({width:'330px',height:'155px'})}}" data-ref="verify" data-event-opts="{{[['^success',[['success']]]]}}" bind:success="__e" bind:__l="__l"></verify><block wx:if="{{isShowAuth}}"><authorize bind:authColse="__e" bind:onLoadFun="__e" vue-id="2ec06c53-2" data-event-opts="{{[['^authColse',[['authColse']]],['^onLoadFun',[['onLoadFun']]]]}}" bind:__l="__l"></authorize></block></view>
<view style="{{(colorStyle)}}"><view class="ChangePassword"><form data-event-opts="{{[['submit',[['editPwd',['$event']]]]]}}" bindsubmit="__e"><view class="phone">{{"当前手机号:"+phone}}</view><view class="list"><view class="item"><input type="password" placeholder="设置新密码" placeholder-class="placeholder" name="password" value="{{password}}"/></view><view class="item"><input type="password" placeholder="确认新密码" placeholder-class="placeholder" name="qr_password" value="{{qr_password}}"/></view><view class="item acea-row row-between-wrapper"><input class="codeIput" type="number" placeholder="填写验证码" placeholder-class="placeholder" maxlength="{{4}}" name="captcha" value="{{captcha}}"/><button class="{{['code','font-num',disabled===true?'on':'']}}" disabled="{{disabled}}" data-event-opts="{{[['tap',[['code',['$event']]]]]}}" bindtap="__e">{{''+text+''}}</button></view></view><button class="confirmBnt bg-color" form-type="submit">确认修改</button></form></view><verify class="vue-ref" vue-id="2ec06c53-1" captchaType="{{captchaType}}" imgSize="{{({width:'330px',height:'155px'})}}" data-ref="verify" data-event-opts="{{[['^success',[['success']]]]}}" bind:success="__e" bind:__l="__l"></verify><block wx:if="{{isShowAuth}}"><authorize bind:authColse="__e" bind:onLoadFun="__e" vue-id="2ec06c53-2" data-event-opts="{{[['^authColse',[['authColse']]],['^onLoadFun',[['onLoadFun']]]]}}" bind:__l="__l"></authorize></block></view>

View File

@@ -1625,6 +1625,8 @@ Route::group('adminapi', function () {
Route::post('import_erp_product', 'v1.product.StoreProduct/import_erp_product')->option(['real_name' => '导入ERP商品到平台']);
//手动同步聚水潭商品
Route::post('sync_jst_product', 'v1.product.StoreProduct/sync_jst_product')->option(['real_name' => '手动同步聚水潭商品']);
//批量上传商品到聚水潭
Route::post('upload_jst_itemsku', 'v1.product.StoreProduct/upload_jst_itemsku')->option(['real_name' => '批量上传商品到聚水潭']);
//保存还未提交数据
Route::post('cache', 'v1.product.StoreProduct/saveCacheData')->option(['real_name' => '保存还未提交数据']);
//删除退出未保存的数据

118
test_db_config.php Normal file
View File

@@ -0,0 +1,118 @@
#!/usr/bin/env php
<?php
namespace think;
require __DIR__ . '/vendor/autoload.php';
echo "========================================\n";
echo "检查数据库聚水潭配置\n";
echo "========================================\n\n";
$app = new App();
$app->initialize();
echo "✅ 应用初始化成功!\n\n";
try {
echo "1️⃣ 查询数据库配置...\n\n";
$configModel = new \app\model\system\config\SystemConfig();
$configs = $configModel->whereIn('name', [
'jst_login_account',
'jst_login_password',
'jst_appkey',
'jst_appsecret'
])->select();
echo "找到以下配置:\n";
foreach ($configs as $config) {
$value = $config['value'];
if (in_array($config['name'], ['jst_login_password', 'jst_appsecret'])) {
echo " - {$config['name']}: " . (empty($value) ? '❌ 未配置' : '✅ 已配置(' . strlen($value) . '位)') . "\n";
} else {
echo " - {$config['name']}: " . (empty($value) ? '❌ 未配置' : $value) . "\n";
}
}
echo "\n";
$loginAccount = $configs->where('name', 'jst_login_account')->value('value', '');
$loginPassword = $configs->where('name', 'jst_login_password')->value('value', '');
if (empty($loginAccount) || empty($loginPassword)) {
echo "⚠️ 数据库中缺少登录凭证\n\n";
echo "建议:\n";
echo "1. 前往后台: 系统设置 → ERP配置\n";
echo "2. 填写聚水潭登录账号和密码\n";
echo "3. 保存配置\n\n";
echo "或者直接在.env文件中添加:\n";
echo "JST_LOGIN_ACCOUNT=你的账号\n";
echo "JST_LOGIN_PASSWORD=你的密码\n\n";
exit(1);
}
echo "✅ 找到登录凭证!\n\n";
echo "2️⃣ 尝试刷新Token...\n\n";
try {
$haichaoJst = \app\haichao\HaichaoJstServices::getServices();
echo "正在调用authLogin...\n";
$haichaoJst->authLogin($loginAccount, $loginPassword);
echo "✅ 登录成功!\n\n";
$envFile = __DIR__ . '/.env';
$envContent = file_get_contents($envFile);
preg_match('/JST_ACCESS_TOKEN\s*=\s*(.*)/', $envContent, $matches);
$newToken = isset($matches[1]) ? trim($matches[1]) : '';
echo "新Token: " . substr($newToken, 0, 20) . "...\n\n";
echo "3️⃣ 测试新Token...\n\n";
$result = $haichaoJst->getProductListStand();
echo "✅ API调用成功!\n\n";
if (!empty($result)) {
echo "📦 获取到 " . count($result) . " 条商品数据\n\n";
if (isset($result[0])) {
echo "📋 示例商品:\n";
$product = $result[0];
echo json_encode($product, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . "\n\n";
}
} else {
echo "⚠️ 未获取到商品数据\n";
echo "可能原因:\n";
echo "1. 聚水潭系统中没有商品\n";
echo "2. 时间范围内没有修改过的商品\n\n";
}
} catch (\Throwable $e) {
echo "❌ 刷新失败: " . $e->getMessage() . "\n\n";
echo "请检查:\n";
echo "1. 聚水潭账号密码是否正确\n";
echo "2. 聚水潭应用是否已授权\n";
echo "3. 应用Key和Secret是否正确\n\n";
}
echo "========================================\n";
echo "完成\n";
echo "========================================\n";
} catch (\Throwable $e) {
echo "\n❌ 发生错误!\n";
echo "错误信息: " . $e->getMessage() . "\n";
echo "错误文件: " . $e->getFile() . ":" . $e->getLine() . "\n";
echo "\n堆栈跟踪:\n" . $e->getTraceAsString() . "\n";
exit(1);
}

40
test_debug.php Normal file
View File

@@ -0,0 +1,40 @@
#!/usr/bin/env php
<?php
namespace think;
require __DIR__ . '/vendor/autoload.php';
echo "========================================\n";
echo "聚水潭商品拉取调试\n";
echo "========================================\n\n";
$app = new App();
$app->initialize();
try {
$haichaoJst = \app\haichao\HaichaoJstServices::getServices();
echo "1️⃣ 测试 getProductListStand()...\n\n";
$result1 = $haichaoJst->getProductListStand();
echo "返回类型: " . gettype($result1) . "\n";
echo "原始数据:\n";
print_r($result1);
echo "\n2️⃣ 测试 getInneropenItemList()...\n\n";
try {
$result2 = $haichaoJst->getInneropenItemList();
echo "返回类型: " . gettype($result2) . "\n";
echo "数据数量: " . count($result2) . "\n";
if (!empty($result2)) {
echo "第一条数据:\n";
print_r($result2[0]);
}
} catch (\Throwable $e) {
echo "❌ 失败: " . $e->getMessage() . "\n";
}
} catch (\Throwable $e) {
echo "\n❌ 错误: " . $e->getMessage() . "\n";
}

123
test_haichao.php Normal file
View File

@@ -0,0 +1,123 @@
#!/usr/bin/env php
<?php
namespace think;
require __DIR__ . '/vendor/autoload.php';
$spu = $argv[1] ?? '';
echo "========================================\n";
echo "聚水潭商品拉取接口测试\n";
echo "========================================\n\n";
$app = new App();
$app->initialize();
echo "✅ 应用初始化成功!\n\n";
try {
echo "1️⃣ 测试HaichaoJstServices方法...\n\n";
try {
$haichaoJst = \app\haichao\HaichaoJstServices::getServices();
if (empty($spu)) {
echo "用法: php test_haichao.php SPU编码\n";
echo "示例: php test_haichao.php test123\n";
echo " php test_haichao.php SPU001,SPU002\n\n";
echo "2️⃣ 尝试获取示例数据(getProductListStand)...\n\n";
try {
echo "正在调用聚水潭API...\n";
$result = $haichaoJst->getProductListStand();
echo "✅ API调用成功!\n\n";
if (empty($result)) {
echo "⚠️ 未获取到任何数据\n";
echo "可能原因:\n";
echo " 1. 聚水潭系统中没有商品\n";
echo " 2. 时间范围内没有修改过的商品\n";
echo " 3. API配置有误\n\n";
} else {
echo "📦 获取到 " . count($result) . " 条商品数据\n\n";
if (!empty($result) && isset($result[0])) {
echo "📋 示例商品:\n";
$product = $result[0];
echo json_encode($product, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . "\n\n";
}
}
} catch (\Throwable $e) {
echo "❌ getProductListStand失败: " . $e->getMessage() . "\n";
echo "错误文件: " . $e->getFile() . ":" . $e->getLine() . "\n\n";
}
exit(0);
}
echo "正在调用HaichaoJstServices::getProductListKuan...\n";
echo "SPU: " . $spu . "\n\n";
$result = $haichaoJst->getProductListKuan($spu);
echo "✅ API调用成功!\n\n";
if (empty($result)) {
echo "⚠️ 未获取到任何数据\n\n";
} else {
echo "📦 获取到的数据:\n\n";
echo json_encode($result, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . "\n\n";
if (isset($result['code'])) {
echo "响应码: " . $result['code'] . "\n";
}
if (isset($result['msg'])) {
echo "响应消息: " . $result['msg'] . "\n";
}
if (isset($result['datas'])) {
echo "商品数量: " . count($result['datas']) . "\n";
if (!empty($result['datas'])) {
echo "\n📋 第一个商品信息:\n";
$firstProduct = $result['datas'][0];
echo " - SPU ID: " . ($firstProduct['i_id'] ?? 'N/A') . "\n";
echo " - 商品名称: " . ($firstProduct['name'] ?? 'N/A') . "\n";
echo " - 售价: " . ($firstProduct['s_price'] ?? 'N/A') . "\n";
echo " - 市场价: " . ($firstProduct['market_price'] ?? 'N/A') . "\n";
echo " - 成本价: " . ($firstProduct['c_price'] ?? 'N/A') . "\n";
if (isset($firstProduct['skus']) && is_array($firstProduct['skus'])) {
echo " - SKU数量: " . count($firstProduct['skus']) . "\n";
if (!empty($firstProduct['skus'])) {
echo "\n 📋 第一个SKU:\n";
$firstSku = $firstProduct['skus'][0];
echo " - SKU ID: " . ($firstSku['sku_id'] ?? 'N/A') . "\n";
echo " - SKU名称: " . ($firstSku['properties_value'] ?? 'N/A') . "\n";
echo " - 售价: " . ($firstSku['sale_price'] ?? 'N/A') . "\n";
}
}
}
}
}
} catch (\Throwable $e) {
echo "❌ HaichaoJstServices测试失败: " . $e->getMessage() . "\n";
echo "错误文件: " . $e->getFile() . ":" . $e->getLine() . "\n";
echo "\n堆栈跟踪:\n" . $e->getTraceAsString() . "\n\n";
}
echo "\n========================================\n";
echo "测试完成\n";
echo "========================================\n";
} catch (\Throwable $e) {
echo "\n❌ 发生错误!\n";
echo "错误信息: " . $e->getMessage() . "\n";
echo "错误文件: " . $e->getFile() . ":" . $e->getLine() . "\n";
echo "\n堆栈跟踪:\n" . $e->getTraceAsString() . "\n";
exit(1);
}

115
test_haichao_all.php Normal file
View File

@@ -0,0 +1,115 @@
#!/usr/bin/env php
<?php
namespace think;
require __DIR__ . '/vendor/autoload.php';
echo "========================================\n";
echo "聚水潭商品拉取测试\n";
echo "========================================\n\n";
$app = new App();
$app->initialize();
echo "✅ 应用初始化成功!\n\n";
try {
$haichaoJst = \app\haichao\HaichaoJstServices::getServices();
echo "1️⃣ 测试全量拉取方法 (getProductList)...\n\n";
$result = $haichaoJst->getProductList();
echo "✅ API调用成功!\n\n";
echo "返回数据结构:\n";
echo " - cursor: " . ($result['cursor'] ?? 'N/A') . "\n";
echo " - data_count: " . ($result['data_count'] ?? 'N/A') . "\n";
echo " - page_index: " . ($result['page_index'] ?? 'N/A') . "\n";
echo " - page_count: " . ($result['page_count'] ?? 'N/A') . "\n";
echo " - page_size: " . ($result['page_size'] ?? 'N/A') . "\n";
echo " - has_next: " . (($result['has_next'] ?? false) ? '是' : '否') . "\n";
if (isset($result['datas']) && !empty($result['datas'])) {
echo "\n📦 获取到 " . count($result['datas']) . " 条商品数据\n\n";
foreach ($result['datas'] as $index => $product) {
echo str_repeat("=", 70) . "\n";
echo "📋 商品 #" . ($index + 1) . "\n";
echo str_repeat("-", 70) . "\n";
echo " SPU ID: " . ($product['i_id'] ?? 'N/A') . "\n";
echo " 商品名称: " . ($product['name'] ?? 'N/A') . "\n";
echo " 售价: " . ($product['s_price'] ?? 'N/A') . "\n";
echo " 市场价: " . ($product['market_price'] ?? 'N/A') . "\n";
echo " 成本价: " . ($product['c_price'] ?? 'N/A') . "\n";
echo " 图片: " . ($product['pic'] ?? 'N/A') . "\n";
if (isset($product['skus']) && is_array($product['skus'])) {
echo " SKU数量: " . count($product['skus']) . "\n";
foreach ($product['skus'] as $skuIndex => $sku) {
echo " SKU #" . ($skuIndex + 1) . ":\n";
echo " - SKU ID: " . ($sku['sku_id'] ?? 'N/A') . "\n";
echo " - 规格: " . ($sku['properties_value'] ?? 'N/A') . "\n";
echo " - 售价: " . ($sku['sale_price'] ?? 'N/A') . "\n";
echo " - 市场价: " . ($sku['market_price'] ?? 'N/A') . "\n";
echo " - 成本价: " . ($sku['cost_price'] ?? 'N/A') . "\n";
}
}
echo "\n";
}
echo str_repeat("=", 70) . "\n";
echo "完整JSON数据:\n";
echo str_repeat("-", 70) . "\n";
echo json_encode($result, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . "\n";
} else {
echo "\n⚠️ datas数组为空\n\n";
echo "可能原因:\n";
echo " 1. 聚水潭系统中没有商品\n";
echo " 2. 时间范围内没有修改过的商品\n";
echo " 3. 拉取配置有问题\n\n";
echo "完整返回:\n";
echo json_encode($result, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . "\n";
}
echo "\n2️⃣ 尝试其他方法...\n\n";
echo "2.1 测试 getProductListShop()...\n";
try {
$result2 = $haichaoJst->getProductListShop();
echo " ✅ 成功\n";
echo " 返回: " . count($result2) . " 条\n";
if (!empty($result2)) {
echo " 第一条: " . json_encode($result2[0] ?? [], JSON_UNESCAPED_UNICODE) . "\n";
}
} catch (\Throwable $e) {
echo " ❌ 失败: " . $e->getMessage() . "\n";
}
echo "\n2.2 测试 getInneropenItemList()...\n";
try {
$result3 = $haichaoJst->getInneropenItemList();
echo " ✅ 成功\n";
echo " 返回: " . count($result3) . " 条\n";
if (!empty($result3)) {
echo " 第一条: " . json_encode($result3[0] ?? [], JSON_UNESCAPED_UNICODE) . "\n";
}
} catch (\Throwable $e) {
echo " ❌ 失败: " . $e->getMessage() . "\n";
}
echo "\n========================================\n";
echo "测试完成\n";
echo "========================================\n";
} catch (\Throwable $e) {
echo "\n❌ 发生错误!\n";
echo "错误信息: " . $e->getMessage() . "\n";
echo "错误文件: " . $e->getFile() . ":" . $e->getLine() . "\n";
echo "\n堆栈跟踪:\n" . $e->getTraceAsString() . "\n";
exit(1);
}

144
test_haichao_detail.php Normal file
View File

@@ -0,0 +1,144 @@
#!/usr/bin/env php
<?php
namespace think;
require __DIR__ . '/vendor/autoload.php';
$spu = $argv[1] ?? '';
echo "========================================\n";
echo "聚水潭商品拉取接口测试\n";
echo "========================================\n\n";
$app = new App();
$app->initialize();
echo "✅ 应用初始化成功!\n\n";
try {
echo "1️⃣ 测试HaichaoJstServices方法...\n\n";
$haichaoJst = \app\haichao\HaichaoJstServices::getServices();
if (empty($spu)) {
echo "用法: php test_haichao_detail.php SPU编码\n";
echo "示例: php test_haichao_detail.php test123\n\n";
echo "2️⃣ 尝试获取示例数据(getProductListStand)...\n\n";
try {
echo "正在调用聚水潭API...\n";
$result = $haichaoJst->getProductListStand();
echo "✅ API调用成功!\n\n";
if (empty($result)) {
echo "⚠️ 未获取到任何数据\n";
echo "可能原因:\n";
echo " 1. 聚水潭系统中没有商品\n";
echo " 2. 时间范围内没有修改过的商品\n\n";
} else {
echo "📦 获取到 " . count($result) . " 条商品数据\n\n";
$index = 1;
foreach ($result as $product) {
echo str_repeat("=", 60) . "\n";
echo "📋 商品 #" . $index . "\n";
echo str_repeat("-", 60) . "\n";
$index++;
echo " SPU ID: " . ($product['i_id'] ?? 'N/A') . "\n";
echo " 商品名称: " . ($product['name'] ?? 'N/A') . "\n";
echo " 售价: " . ($product['s_price'] ?? 'N/A') . "\n";
echo " 市场价: " . ($product['market_price'] ?? 'N/A') . "\n";
echo " 成本价: " . ($product['c_price'] ?? 'N/A') . "\n";
echo " 图片: " . ($product['pic'] ?? 'N/A') . "\n";
if (isset($product['skus']) && is_array($product['skus'])) {
echo " SKU数量: " . count($product['skus']) . "\n";
$skuIndex = 1;
foreach ($product['skus'] as $sku) {
echo " SKU #" . $skuIndex . ":\n";
$skuIndex++;
echo " - SKU ID: " . ($sku['sku_id'] ?? 'N/A') . "\n";
echo " - 规格: " . ($sku['properties_value'] ?? 'N/A') . "\n";
echo " - 售价: " . ($sku['sale_price'] ?? 'N/A') . "\n";
echo " - 市场价: " . ($sku['market_price'] ?? 'N/A') . "\n";
echo " - 成本价: " . ($sku['cost_price'] ?? 'N/A') . "\n";
}
}
echo "\n";
}
echo str_repeat("=", 60) . "\n";
echo "完整JSON数据:\n";
echo str_repeat("-", 60) . "\n";
echo json_encode($result, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . "\n";
}
} catch (\Throwable $e) {
echo "❌ getProductListStand失败: " . $e->getMessage() . "\n";
echo "错误文件: " . $e->getFile() . ":" . $e->getLine() . "\n\n";
}
exit(0);
}
echo "正在调用HaichaoJstServices::getProductListKuan...\n";
echo "SPU: " . $spu . "\n\n";
$result = $haichaoJst->getProductListKuan($spu);
echo "✅ API调用成功!\n\n";
if (empty($result)) {
echo "⚠️ 未获取到任何数据\n\n";
} else {
echo "📦 获取到的数据:\n\n";
echo json_encode($result, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . "\n\n";
if (isset($result['code'])) {
echo "响应码: " . $result['code'] . "\n";
}
if (isset($result['msg'])) {
echo "响应消息: " . $result['msg'] . "\n";
}
if (isset($result['datas'])) {
echo "商品数量: " . count($result['datas']) . "\n";
if (!empty($result['datas'])) {
echo "\n📋 第一个商品信息:\n";
$firstProduct = $result['datas'][0];
echo " - SPU ID: " . ($firstProduct['i_id'] ?? 'N/A') . "\n";
echo " - 商品名称: " . ($firstProduct['name'] ?? 'N/A') . "\n";
echo " - 售价: " . ($firstProduct['s_price'] ?? 'N/A') . "\n";
echo " - 市场价: " . ($firstProduct['market_price'] ?? 'N/A') . "\n";
echo " - 成本价: " . ($firstProduct['c_price'] ?? 'N/A') . "\n";
if (isset($firstProduct['skus']) && is_array($firstProduct['skus'])) {
echo " - SKU数量: " . count($firstProduct['skus']) . "\n";
if (!empty($firstProduct['skus'])) {
echo "\n 📋 第一个SKU:\n";
$firstSku = $firstProduct['skus'][0];
echo " - SKU ID: " . ($firstSku['sku_id'] ?? 'N/A') . "\n";
echo " - SKU名称: " . ($firstSku['properties_value'] ?? 'N/A') . "\n";
echo " - 售价: " . ($firstSku['sale_price'] ?? 'N/A') . "\n";
}
}
}
}
}
echo "\n========================================\n";
echo "测试完成\n";
echo "========================================\n";
} catch (\Throwable $e) {
echo "\n❌ 发生错误!\n";
echo "错误信息: " . $e->getMessage() . "\n";
echo "错误文件: " . $e->getFile() . ":" . $e->getLine() . "\n";
echo "\n堆栈跟踪:\n" . $e->getTraceAsString() . "\n";
exit(1);
}

85
test_haichao_json.php Normal file
View File

@@ -0,0 +1,85 @@
#!/usr/bin/env php
<?php
namespace think;
require __DIR__ . '/vendor/autoload.php';
$spu = $argv[1] ?? '';
echo "========================================\n";
echo "聚水潭商品拉取接口测试\n";
echo "========================================\n\n";
$app = new App();
$app->initialize();
echo "✅ 应用初始化成功!\n\n";
try {
$haichaoJst = \app\haichao\HaichaoJstServices::getServices();
if (empty($spu)) {
echo "测试 getProductListStand 方法...\n\n";
$result = $haichaoJst->getProductListStand();
echo "✅ API调用成功!\n\n";
echo "返回数据类型: " . gettype($result) . "\n";
if (is_array($result)) {
echo "顶层键值:\n";
foreach (array_keys($result) as $key) {
echo " - {$key}\n";
}
if (isset($result['datas']) && is_array($result['datas'])) {
echo "\n📦 datas数组包含 " . count($result['datas']) . " 个元素\n\n";
if (!empty($result['datas'])) {
echo "第一个元素:\n";
print_r($result['datas'][0]);
} else {
echo "⚠️ datas数组为空\n";
}
} else if (isset($result[0])) {
echo "\n📦 第一个元素:\n";
print_r($result[0]);
} else {
echo "\n完整返回:\n";
print_r($result);
}
} else {
echo "\n完整返回:\n";
print_r($result);
}
echo "\n\n";
echo "完整JSON:\n";
echo json_encode($result, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
echo "\n";
} else {
echo "测试 getProductListKuan 方法...\n\n";
echo "SPU: " . $spu . "\n\n";
$result = $haichaoJst->getProductListKuan($spu);
echo "✅ API调用成功!\n\n";
echo "完整JSON:\n";
echo json_encode($result, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
echo "\n";
}
echo "\n========================================\n";
echo "测试完成\n";
echo "========================================\n";
} catch (\Throwable $e) {
echo "\n❌ 发生错误!\n";
echo "错误信息: " . $e->getMessage() . "\n";
echo "错误文件: " . $e->getFile() . ":" . $e->getLine() . "\n";
echo "\n堆栈跟踪:\n" . $e->getTraceAsString() . "\n";
exit(1);
}

83
test_jst_final_demo.php Normal file
View File

@@ -0,0 +1,83 @@
#!/usr/bin/env php
<?php
namespace think;
require __DIR__ . '/vendor/autoload.php';
echo "========================================\n";
echo "聚水潭商品拉取完整测试\n";
echo "========================================\n\n";
$app = new App();
$app->initialize();
echo "✅ 应用初始化成功!\n\n";
try {
$haichaoJst = \app\haichao\HaichaoJstServices::getServices();
echo "正在从聚水潭拉取商品数据...\n\n";
$result = $haichaoJst->getProductListStand();
echo "✅ API调用成功!\n\n";
if (empty($result)) {
echo "⚠️ 未获取到任何商品数据\n\n";
exit(0);
}
echo "📊 统计信息:\n";
echo " - 获取商品数量: " . count($result) . " 个\n\n";
foreach ($result as $index => $product) {
$num = (int)$index + 1;
echo str_repeat("=", 80) . "\n";
echo "商品 #{$num}\n";
echo str_repeat("-", 80) . "\n";
echo "【基本信息】\n";
echo " SPU ID: " . ($product['i_id'] ?? 'N/A') . "\n";
echo " 商品名称: " . ($product['name'] ?? 'N/A') . "\n";
echo " 售价: ¥" . ($product['s_price'] ?? 'N/A') . "\n";
echo " 市场价: ¥" . ($product['market_price'] ?? 'N/A') . "\n";
echo " 成本价: ¥" . ($product['c_price'] ?? 'N/A') . "\n";
echo " 图片URL: " . ($product['pic'] ?? 'N/A') . "\n";
if (isset($product['skus']) && is_array($product['skus']) && count($product['skus']) > 0) {
echo "\n【SKU规格】\n";
echo " SKU数量: " . count($product['skus']) . " 个\n\n";
foreach ($product['skus'] as $skuIndex => $sku) {
$skuNum = $skuIndex + 1;
echo " ┌─ SKU #{$skuNum}\n";
echo " │ SKU ID: " . ($sku['sku_id'] ?? 'N/A') . "\n";
echo " │ 规格属性: " . ($sku['properties_value'] ?? 'N/A') . "\n";
echo " │ 售价: ¥" . ($sku['sale_price'] ?? 'N/A') . "\n";
echo " │ 市场价: ¥" . ($sku['market_price'] ?? 'N/A') . "\n";
echo " │ 成本价: ¥" . ($sku['cost_price'] ?? 'N/A') . "\n";
echo " └─────────────────────────────────────────\n";
}
}
echo "\n";
}
echo str_repeat("=", 80) . "\n";
echo "完整JSON数据:\n";
echo str_repeat("-", 80) . "\n";
echo json_encode($result, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . "\n\n";
echo "========================================\n";
echo "✅ 测试完成!成功拉取 " . count($result) . " 个商品\n";
echo "========================================\n";
} catch (\Throwable $e) {
echo "\n❌ 发生错误!\n";
echo "错误信息: " . $e->getMessage() . "\n";
echo "错误文件: " . $e->getFile() . ":" . $e->getLine() . "\n";
echo "\n堆栈跟踪:\n" . $e->getTraceAsString() . "\n";
exit(1);
}

71
test_simple.php Normal file
View File

@@ -0,0 +1,71 @@
#!/usr/bin/env php
<?php
namespace think;
require __DIR__ . '/vendor/autoload.php';
echo "========================================\n";
echo "快速检查配置\n";
echo "========================================\n\n";
$app = new App();
$app->initialize();
echo "✅ 应用初始化成功!\n\n";
try {
$db = \think\facade\Db::connect();
$configs = $db->query("SELECT * FROM eb_system_config WHERE `name` IN ('jst_login_account', 'jst_login_password', 'jst_appkey', 'jst_appsecret')");
echo "数据库配置:\n";
foreach ($configs as $config) {
$name = $config['name'] ?? 'N/A';
$value = $config['value'] ?? '';
if (in_array($name, ['jst_login_password', 'jst_appsecret'])) {
echo " - {$name}: " . (empty($value) ? '❌ 未配置' : '✅ 已配置(' . strlen($value) . '位)') . "\n";
} else {
echo " - {$name}: " . (empty($value) ? '❌ 未配置' : $value) . "\n";
}
}
echo "\n";
$loginAccount = $configs[array_search('jst_login_account', array_column($configs, 'name'))]['value'] ?? '';
$loginPassword = $configs[array_search('jst_login_password', array_column($configs, 'name'))]['value'] ?? '';
if (empty($loginAccount) || empty($loginPassword)) {
echo "⚠️ 数据库中缺少登录凭证\n\n";
echo "请先在后台ERP配置中填写账号密码\n\n";
exit(0);
}
echo "✅ 找到登录凭证!\n";
echo "尝试刷新Token...\n\n";
try {
$haichaoJst = \app\haichao\HaichaoJstServices::getServices();
$haichaoJst->authLogin($loginAccount, $loginPassword);
echo "✅ 登录成功!\n\n";
echo "正在测试商品拉取...\n";
$result = $haichaoJst->getProductListStand();
if (!empty($result)) {
echo "✅ 成功获取到 " . count($result) . " 条商品!\n\n";
echo json_encode($result[0] ?? [], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
echo "\n\n";
} else {
echo "⚠️ 未获取到商品\n";
}
} catch (\Throwable $e) {
echo "❌ 失败: " . $e->getMessage() . "\n\n";
}
} catch (\Throwable $e) {
echo "错误: " . $e->getMessage() . "\n";
}

14
test_table.php Normal file
View File

@@ -0,0 +1,14 @@
#!/usr/bin/env php
<?php
namespace think;
require __DIR__ . '/vendor/autoload.php';
$app = new App();
$app->initialize();
$db = \think\facade\Db::connect();
$configs = $db->query("DESCRIBE eb_system_config");
print_r($configs);

130
test_token.php Normal file
View File

@@ -0,0 +1,130 @@
#!/usr/bin/env php
<?php
namespace think;
require __DIR__ . '/vendor/autoload.php';
echo "========================================\n";
echo "聚水潭Token状态检查\n";
echo "========================================\n\n";
$app = new App();
$app->initialize();
echo "✅ 应用初始化成功!\n\n";
try {
echo "1️⃣ 读取当前配置...\n\n";
$envFile = __DIR__ . '/.env';
$envContent = file_get_contents($envFile);
preg_match('/JST_APP_KEY\s*=\s*(.*)/', $envContent, $matches);
$appKey = isset($matches[1]) ? trim($matches[1]) : '';
preg_match('/JST_APP_SECRET\s*=\s*(.*)/', $envContent, $matches);
$appSecret = isset($matches[1]) ? trim($matches[1]) : '';
preg_match('/JST_ACCESS_TOKEN\s*=\s*(.*)/', $envContent, $matches);
$accessToken = isset($matches[1]) ? trim($matches[1]) : '';
echo "JST_APP_KEY: " . $appKey . "\n";
echo "JST_APP_SECRET: " . (empty($appSecret) ? '❌ 未配置' : '✅ 已配置') . "\n";
echo "JST_ACCESS_TOKEN: " . (empty($accessToken) ? '❌ 未配置' : substr($accessToken, 0, 20) . '...') . "\n\n";
if (empty($appKey) || empty($appSecret) || empty($accessToken)) {
echo "⚠️ 配置不完整,请检查.env文件\n\n";
exit(1);
}
echo "2️⃣ 测试当前Token是否有效...\n\n";
try {
$haichaoJst = \app\haichao\HaichaoJstServices::getServices();
echo "正在调用API测试Token...\n";
$result = $haichaoJst->getProductListStand();
echo "✅ Token有效!\n\n";
if (!empty($result)) {
echo "📦 获取到 " . count($result) . " 条商品数据\n\n";
if (isset($result[0])) {
echo "📋 示例商品:\n";
$product = $result[0];
echo json_encode($product, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . "\n\n";
}
}
} catch (\Throwable $e) {
echo "❌ Token无效: " . $e->getMessage() . "\n\n";
echo "3️⃣ 尝试通过用户名密码登录获取新Token...\n\n";
preg_match('/JST_LOGIN_ACCOUNT\s*=\s*(.*)/', $envContent, $matches);
$loginAccount = isset($matches[1]) ? trim($matches[1]) : '';
preg_match('/JST_LOGIN_PASSWORD\s*=\s*(.*)/', $envContent, $matches);
$loginPassword = isset($matches[1]) ? trim($matches[1]) : '';
echo "登录账号: " . (empty($loginAccount) ? '❌ 未配置' : $loginAccount) . "\n";
echo "登录密码: " . (empty($loginPassword) ? '❌ 未配置' : '✅ 已配置') . "\n\n";
if (empty($loginAccount) || empty($loginPassword)) {
echo "⚠️ 无法刷新Token,缺少登录凭证\n";
echo "请在.env文件中添加:\n";
echo "JST_LOGIN_ACCOUNT=你的聚水潭账号\n";
echo "JST_LOGIN_PASSWORD=你的聚水潭密码\n\n";
exit(1);
}
echo "正在尝试登录...\n";
try {
$haichaoJst->authLogin($loginAccount, $loginPassword);
$envContent = file_get_contents($envFile);
preg_match('/JST_ACCESS_TOKEN\s*=\s*(.*)/', $envContent, $matches);
$newToken = isset($matches[1]) ? trim($matches[1]) : '';
echo "✅ 登录成功! 新Token: " . substr($newToken, 0, 20) . "...\n\n";
echo "4️⃣ 再次测试API...\n\n";
$result = $haichaoJst->getProductListStand();
echo "✅ API调用成功!\n\n";
if (!empty($result)) {
echo "📦 获取到 " . count($result) . " 条商品数据\n\n";
if (isset($result[0])) {
echo "📋 示例商品:\n";
$product = $result[0];
echo json_encode($product, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . "\n\n";
}
}
} catch (\Throwable $e2) {
echo "❌ 登录失败: " . $e2->getMessage() . "\n\n";
echo "请检查:\n";
echo "1. 聚水潭账号密码是否正确\n";
echo "2. 聚水潭应用是否已授权\n";
echo "3. 应用Key和Secret是否正确\n\n";
}
}
echo "========================================\n";
echo "检查完成\n";
echo "========================================\n";
} catch (\Throwable $e) {
echo "\n❌ 发生错误!\n";
echo "错误信息: " . $e->getMessage() . "\n";
echo "错误文件: " . $e->getFile() . ":" . $e->getLine() . "\n";
echo "\n堆栈跟踪:\n" . $e->getTraceAsString() . "\n";
exit(1);
}

View File

@@ -1,5 +1,9 @@
# CHANGELOG
## 1.2.0 - 2024-10-17
- Refactor all credentials providers.
## 1.1.3 - 2020-12-24
- Require guzzle ^6.3|^7.0

View File

@@ -1,41 +1,57 @@
[English](/README.md) | 简体中文
![](https://aliyunsdk-pages.alicdn.com/icons/AlibabaCloud.svg)
# Alibaba Cloud Credentials for PHP
[![PHP CI](https://github.com/aliyun/credentials-php/actions/workflows/ci.yml/badge.svg)](https://github.com/aliyun/credentials-php/actions/workflows/ci.yml)
[![codecov](https://codecov.io/gh/aliyun/credentials-php/graph/badge.svg?token=YIkSjtfKbB)](https://codecov.io/gh/aliyun/credentials-php)
[![Latest Stable Version](https://poser.pugx.org/alibabacloud/credentials/v/stable)](https://packagist.org/packages/alibabacloud/credentials)
[![composer.lock](https://poser.pugx.org/alibabacloud/credentials/composerlock)](https://packagist.org/packages/alibabacloud/credentials)
[![Total Downloads](https://poser.pugx.org/alibabacloud/credentials/downloads)](https://packagist.org/packages/alibabacloud/credentials)
[![License](https://poser.pugx.org/alibabacloud/credentials/license)](https://packagist.org/packages/alibabacloud/credentials)
[![codecov](https://codecov.io/gh/aliyun/credentials-php/branch/master/graph/badge.svg)](https://codecov.io/gh/aliyun/credentials-php)
[![Travis Build Status](https://travis-ci.org/aliyun/credentials-php.svg?branch=master)](https://travis-ci.org/aliyun/credentials-php)
[![Appveyor Build Status](https://ci.appveyor.com/api/projects/status/6jxpwmhyfipagtge/branch/master?svg=true)](https://ci.appveyor.com/project/aliyun/credentials-php)
![](https://aliyunsdk-pages.alicdn.com/icons/AlibabaCloud.svg)
Alibaba Cloud Credentials for PHP 是帮助 PHP 开发者管理凭据的工具。
## 先决条件
您的系统需要满足[先决条件](/docs/zh-CN/0-Prerequisites.md),包括 PHP> = 5.6。 我们强烈建议使用cURL扩展,并使用TLS后端编译cURL 7.16.2+。
## 安装依赖
如果已在系统上[全局安装 Composer](https://getcomposer.org/doc/00-intro.md#globally),请直接在项目目录中运行以下内容来安装 Alibaba Cloud Credentials for PHP 作为依赖项:
```
```sh
composer require alibabacloud/credentials
```
> 一些用户可能由于网络问题无法安装,可以使用[阿里云 Composer 全量镜像](https://developer.aliyun.com/composer)。
请看[安装](/docs/zh-CN/1-Installation.md)有关通过 Composer 和其他方式安装的详细信息。
## 快速使用
在您开始之前,您需要注册阿里云帐户并获取您的[凭证](https://usercenter.console.aliyun.com/#/manage/ak)。
### 凭证类型
#### 使用默认凭据链
当您在初始化凭据客户端不传入任何参数时,Credentials工具会使用默认凭据链方式初始化客户端。默认凭据的读取逻辑请参见[默认凭据链](#默认凭证提供程序链)。
```php
<?php
use AlibabaCloud\Credentials\Credential;
// Chain Provider if no Parameter
$client = new Credential();
$credential = $client->getCredential();
$credential->getAccessKeyId();
$credential->getAccessKeySecret();
$credential->getSecurityToken();
```
#### AccessKey
通过[用户信息管理][ak]设置 access_key,它们具有该账户完全的权限,请妥善保管。有时出于安全考虑,您不能把具有完全访问权限的主账户 AccessKey 交于一个项目的开发者使用,您可以[创建RAM子账户][ram]并为子账户[授权][permissions],使用RAM子用户的 AccessKey 来进行API调用。
@@ -44,20 +60,19 @@ composer require alibabacloud/credentials
<?php
use AlibabaCloud\Credentials\Credential;
// Chain Provider if no Parameter
$credential = new Credential();
$credential->getAccessKeyId();
$credential->getAccessKeySecret();
use AlibabaCloud\Credentials\Credential\Config;
// Access Key
$ak = new Credential([
$config = new Config([
'type' => 'access_key',
'access_key_id' => '<access_key_id>',
'access_key_secret' => '<access_key_secret>',
'accessKeyId' => '<access_key_id>',
'accessKeySecret' => '<access_key_secret>',
]);
$ak->getAccessKeyId();
$ak->getAccessKeySecret();
$client = new Credential($config);
$credential = $client->getCredential();
$credential->getAccessKeyId();
$credential->getAccessKeySecret();
```
#### STS
@@ -68,102 +83,256 @@ $ak->getAccessKeySecret();
<?php
use AlibabaCloud\Credentials\Credential;
use AlibabaCloud\Credentials\Credential\Config;
$sts = new Credential([
$config = new Config([
'type' => 'sts',
'access_key_id' => '<access_key_id>',
'accessKey_secret' => '<accessKey_secret>',
'security_token' => '<security_token>',
'accessKeyId' => '<access_key_id>',
'accessKeySecret' => '<access_key_secret>',
'securityToken' => '<security_token>',
]);
$sts->getAccessKeyId();
$sts->getAccessKeySecret();
$sts->getSecurityToken();
$client = new Credential($config);
$credential = $client->getCredential();
$credential->getAccessKeyId();
$credential->getAccessKeySecret();
$credential->getSecurityToken();
```
#### RamRoleArn
通过指定[RAM角色][RAM Role],让凭证自动申请维护 STS Token。你可以通过为 `Policy` 赋值来限制获取到的 STS Token 的权限。
通过指定RAM角色的ARN(Alibabacloud Resource Name),Credentials工具可以帮助开发者前往STS换取STS Token。您也可以通过为 `Policy` 赋值来限制RAM角色到一个更小的权限集合。
```php
<?php
use AlibabaCloud\Credentials\Credential;
use AlibabaCloud\Credentials\Credential\Config;
$ramRoleArn = new Credential([
$config = new Config([
'type' => 'ram_role_arn',
'access_key_id' => '<access_key_id>',
'access_key_secret' => '<access_key_secret>',
'role_arn' => '<role_arn>',
'role_session_name' => '<role_session_name>',
'accessKeyId' => '<access_key_id>',
'accessKeySecret' => '<access_key_secret>',
// 要扮演的RAM角色ARN,示例值:acs:ram::123456789012****:role/adminrole,可以通过环境变量ALIBABA_CLOUD_ROLE_ARN设置role_arn
'roleArn' => '<role_arn>',
// 角色会话名称,可以通过环境变量ALIBABA_CLOUD_ROLE_SESSION_NAME设置role_session_name
'roleSessionName' => '<role_session_name>',
// 设置更小的权限策略,非必填。示例值:{"Statement": [{"Action": ["*"],"Effect": "Allow","Resource": ["*"]}],"Version":"1"}
'policy' => '',
// 设置session过期时间,非必填。
'roleSessionExpiration' => 3600,
]);
$ramRoleArn->getAccessKeyId();
$ramRoleArn->getAccessKeySecret();
$ramRoleArn->getRoleArn();
$ramRoleArn->getRoleSessionName();
$ramRoleArn->getPolicy();
$client = new Credential($config);
$credential = $client->getCredential();
$credential->getAccessKeyId();
$credential->getAccessKeySecret();
$credential->getSecurityToken();
```
#### EcsRamRole
通过指定角色名称,让凭证自动申请维护 STS Token
ECS和ECI实例均支持绑定实例RAM角色,当在实例中使用Credentials工具时,将自动获取实例绑定的RAM角色,并通过访问元数据服务获取RAM角色的STS Token,以完成凭据客户端的初始化。
实例元数据服务器支持加固模式和普通模式两种访问方式,Credentials工具默认使用加固模式(IMDSv2)获取访问凭据。若使用加固模式时发生异常,您可以通过设置disableIMDSv1来执行不同的异常处理逻辑:
- 当值为false(默认值)时,会使用普通模式继续获取访问凭据。
- 当值为true时,表示只能使用加固模式获取访问凭据,会抛出异常。
服务端是否支持IMDSv2,取决于您在服务器的配置。
```php
<?php
use AlibabaCloud\Credentials\Credential;
use AlibabaCloud\Credentials\Credential\Config;
$ecsRamRole = new Credential([
$config = new Config([
'type' => 'ecs_ram_role',
'role_name' => '<role_name>',
// 选填,该ECS角色的角色名称,不填会自动获取,但是建议加上以减少请求次数,可以通过环境变量ALIBABA_CLOUD_ECS_METADATA设置role_name
'roleName' => '<role_name>',
// 选填,是否强制关闭IMDSv1,即必须使用IMDSv2加固模式,可以通过环境变量ALIBABA_CLOUD_IMDSV1_DISABLED设置
'disableIMDSv1' => true,
]);
$ecsRamRole->getRoleName();
// Note: `role_name` is optional. It will be retrieved automatically if not set. It is highly recommended to set it up to reduce requests.
$client = new Credential($config);
$credential = $client->getCredential();
$credential->getAccessKeyId();
$credential->getAccessKeySecret();
$credential->getSecurityToken();
```
#### RsaKeyPair
#### OIDCRoleArn
通过指定公钥Id和私钥文件,让凭证自动申请维护 AccessKey。仅支持日本站。
在容器服务 Kubernetes 版中设置了Worker节点RAM角色后,对应节点内的Pod中的应用也就可以像ECS上部署的应用一样,通过元数据服务(Meta Data Server)获取关联角色的STS Token。但如果容器集群上部署的是不可信的应用(比如部署您的客户提交的应用,代码也没有对您开放),您可能并不希望它们能通过元数据服务获取Worker节点关联实例RAM角色的STS Token。为了避免影响云上资源的安全,同时又能让这些不可信的应用安全地获取所需的 STS Token,实现应用级别的权限最小化,您可以使用RRSA(RAM Roles for Service Account)功能。阿里云容器集群会为不同的应用Pod创建和挂载相应的服务账户OIDC Token文件,并将相关配置信息注入到环境变量中,Credentials工具通过获取环境变量的配置信息,调用STS服务的AssumeRoleWithOIDC - OIDC角色SSO时获取扮演角色的临时身份凭证接口换取绑定角色的STS Token。详情请参见[通过RRSA配置ServiceAccount的RAM权限实现Pod权限隔离](https://help.aliyun.com/zh/ack/ack-managed-and-ack-dedicated/user-guide/use-rrsa-to-authorize-pods-to-access-different-cloud-services#task-2142941)。
```php
<?php
use AlibabaCloud\Credentials\Credential;
use AlibabaCloud\Credentials\Credential\Config;
$rsaKeyPair = new Credential([
'type' => 'rsa_key_pair',
'public_key_id' => '<public_key_id>',
'private_key_file' => '<private_key_file>',
$config = new Config([
'type' => 'oidc_role_arn',
// OIDC提供商ARN,可以通过环境变量ALIBABA_CLOUD_OIDC_PROVIDER_ARN设置oidc_provider_arn
'oidcProviderArn' => '<oidc_provider_arn>',
// OIDC Token文件路径,可以通过环境变量ALIBABA_CLOUD_OIDC_TOKEN_FILE设置oidc_token_file_path
'oidcTokenFilePath' => '<oidc_token_file_path>',
// 要扮演的RAM角色ARN,示例值:acs:ram::123456789012****:role/adminrole,可以通过环境变量ALIBABA_CLOUD_ROLE_ARN设置role_arn
'roleArn' => '<role_arn>',
// 角色会话名称,可以通过环境变量ALIBABA_CLOUD_ROLE_SESSION_NAME设置role_session_name
'roleSessionName' => '<role_session_name>',
// 设置更小的权限策略,非必填。示例值:{"Statement": [{"Action": ["*"],"Effect": "Allow","Resource": ["*"]}],"Version":"1"}
'policy' => '',
# 设置session过期时间
'roleSessionExpiration' => 3600,
]);
$rsaKeyPair->getPublicKeyId();
$rsaKeyPair->getPrivateKey();
$client = new Credential($config);
$credential = $client->getCredential();
$credential->getAccessKeyId();
$credential->getAccessKeySecret();
$credential->getSecurityToken();
```
#### Credentials URI
通过指定提供凭证的自定义网络服务地址,让凭证自动申请维护 STS Token。
```php
<?php
use AlibabaCloud\Credentials\Credential;
use AlibabaCloud\Credentials\Credential\Config;
$config = new Config([
'type' => 'credentials_uri',
// 凭证的 URI,格式为http://local_or_remote_uri/,可以通过环境变量ALIBABA_CLOUD_CREDENTIALS_URI设置credentials_uri
'credentialsURI' => '<credentials_uri>',
]);
$client = new Credential($config);
$credential = $client->getCredential();
$credential->getAccessKeyId();
$credential->getAccessKeySecret();
$credential->getSecurityToken();
```
#### Bearer Token
如呼叫中心(CCC)需用此凭证,请自行申请维护 Bearer Token。
目前只有云呼叫中心 CCC 这款产品支持 Bearer Token 的凭据初始化方式。
```php
<?php
use AlibabaCloud\Credentials\Credential;
use AlibabaCloud\Credentials\Credential\Config;
$bearerToken = new Credential([
'type' => 'bearer_token',
'bearer_token' => '<bearer_token>',
$config = new Config([
'type' => 'bearer',
// 填入您的Bearer Token
'bearerToken' => '<bearer_token>',
]);
$bearerToken->getBearerToken();
$bearerToken->getSignature();
$client = new Credential($config);
$credential = $client->getCredential();
$credential->getBearerToken();
```
## 默认凭证提供程序链
默认凭证提供程序链查找可用的凭证,寻找顺序如下:
### 1. 环境凭证
程序首先会在环境变量里寻找环境凭证,如果定义了 `ALIBABA_CLOUD_ACCESS_KEY_ID` 和 `ALIBABA_CLOUD_ACCESS_KEY_SECRET` 环境变量且不为空,程序将使用他们创建默认凭证。
当您的程序开发环境和生产环境采用不同的凭据类型,常见做法是在代码中获取当前环境信息,编写获取不同凭据的分支代码。借助Credentials工具的默认凭据链,您可以用同一套代码,通过程序之外的配置来控制不同环境下的凭据获取方式。当您在不传入参数的情况下,直接使用$credential = new Credential();初始化凭据客户端时,阿里云SDK将会尝试按照如下顺序查找相关凭据信息。
### 2. 配置文件
> 如果用户主目录存在默认文件 `~/.alibabacloud/credentials` (Windows 为 `C:\Users\USER_NAME\.alibabacloud\credentials`),程序会自动创建指定类型和名称的凭证。默认文件可以不存在,但解析错误会抛出异常。 凭证名称不分大小写,若凭证同名,后者会覆盖前者。不同的项目、工具之间可以共用这个配置文件,因为超出项目之外,也不会被意外提交到版本控制。Windows 上可以使用环境变量引用到主目录 %UserProfile%。类 Unix 的系统可以使用环境变量 $HOME 或 ~ (tilde)。 可以通过定义 `ALIBABA_CLOUD_CREDENTIALS_FILE` 环境变量修改默认文件的路径。
### 1. 使用环境变量
Credentials工具会优先在环境变量中获取凭据信息。
- 如果系统环境变量 `ALIBABA_CLOUD_ACCESS_KEY_ID`(密钥Key) 和 `ALIBABA_CLOUD_ACCESS_KEY_SECRET`(密钥Value) 不为空,Credentials工具会优先使用它们作为默认凭据。
- 如果系统环境变量 `ALIBABA_CLOUD_ACCESS_KEY_ID`(密钥Key)、`ALIBABA_CLOUD_ACCESS_KEY_SECRET`(密钥Value)、`ALIBABA_CLOUD_SECURITY_TOKEN`(Token)均不为空,Credentials工具会优先使用STS Token作为默认凭据。
### 2. 使用OIDC RAM角色
若不存在优先级更高的凭据信息,Credentials工具会在环境变量中获取如下内容:
`ALIBABA_CLOUD_ROLE_ARN`:RAM角色名称ARN;
`ALIBABA_CLOUD_OIDC_PROVIDER_ARN`:OIDC提供商ARN;
`ALIBABA_CLOUD_OIDC_TOKEN_FILE`:OIDC Token文件路径;
若以上三个环境变量都已设置内容,Credentials将会使用变量内容调用STS服务的[AssumeRoleWithOIDC - OIDC角色SSO时获取扮演角色的临时身份凭证](https://help.aliyun.com/zh/ram/developer-reference/api-sts-2015-04-01-assumerolewithoidc)接口换取STS Token作为默认凭据。
### 3. 使用 Aliyun CLI 工具的 config.json 配置文件
若不存在优先级更高的凭据信息,Credentials工具会优先在如下位置查找 `config.json` 文件是否存在:
Linux系统:`~/.aliyun/config.json`
Windows系统: `C:\Users\USER_NAME\.aliyun\config.json`
如果文件存在,程序将会使用配置文件中 `current` 指定的凭据信息初始化凭据客户端。当然,您也可以通过环境变量 `ALIBABA_CLOUD_PROFILE` 来指定凭据信息,例如设置 `ALIBABA_CLOUD_PROFILE` 的值为 `AK`。
在config.json配置文件中每个module的值代表了不同的凭据信息获取方式:
- AK:使用用户的Access Key作为凭据信息;
- RamRoleArn:使用RAM角色的ARN来获取凭据信息;
- EcsRamRole:利用ECS绑定的RAM角色来获取凭据信息;
- OIDC:通过OIDC ARN和OIDC Token来获取凭据信息;
- ChainableRamRoleArn:采用角色链的方式,通过指定JSON文件中的其他凭据,以重新获取新的凭据信息。
配置示例信息如下:
```json
{
"current": "AK",
"profiles": [
{
"name": "AK",
"mode": "AK",
"access_key_id": "access_key_id",
"access_key_secret": "access_key_secret"
},
{
"name": "RamRoleArn",
"mode": "RamRoleArn",
"access_key_id": "access_key_id",
"access_key_secret": "access_key_secret",
"ram_role_arn": "ram_role_arn",
"ram_session_name": "ram_session_name",
"expired_seconds": 3600,
"sts_region": "cn-hangzhou"
},
{
"name": "EcsRamRole",
"mode": "EcsRamRole",
"ram_role_name": "ram_role_name"
},
{
"name": "OIDC",
"mode": "OIDC",
"ram_role_arn": "ram_role_arn",
"oidc_token_file": "path/to/oidc/file",
"oidc_provider_arn": "oidc_provider_arn",
"ram_session_name": "ram_session_name",
"expired_seconds": 3600,
"sts_region": "cn-hangzhou"
},
{
"name": "ChainableRamRoleArn",
"mode": "ChainableRamRoleArn",
"source_profile": "AK",
"ram_role_arn": "ram_role_arn",
"ram_session_name": "ram_session_name",
"expired_seconds": 3600,
"sts_region": "cn-hangzhou"
}
]
}
```
### 4. 使用配置文件
>
> 如果用户主目录存在默认文件 `~/.alibabacloud/credentials` (Windows 为 `C:\Users\USER_NAME\.alibabacloud\credentials`),程序会自动创建指定类型和名称的凭证。您也可通过环境变量 `ALIBABA_CLOUD_CREDENTIALS_FILE` 指定配置文件路径。如果文件存在,程序将会使用配置文件中 default 指定的凭据信息初始化凭据客户端。当然,您也可以通过环境变量 `ALIBABA_CLOUD_PROFILE` 来指定凭据信息,例如设置 `ALIBABA_CLOUD_PROFILE` 的值为 `client1`。
配置示例信息如下:
```ini
[default]
@@ -183,68 +352,72 @@ role_arn = role_arn
role_session_name = session_name
[project3]
type = rsa_key_pair # 认证方式为 rsa_key_pair
public_key_id = publicKeyId # Public Key ID
private_key_file = /your/pk.pem # Private Key 文件
type=oidc_role_arn # 认证方式为 oidc_role_arn
oidc_provider_arn=oidc_provider_arn
oidc_token_file_path=oidc_token_file_path
role_arn=role_arn
role_session_name=session_name
```
### 3. 实例 RAM 角色
如果定义了环境变量 `ALIBABA_CLOUD_ECS_METADATA` 且不为空,程序会将该环境变量的值作为角色名称,请求 `http://100.100.100.200/latest/meta-data/ram/security-credentials/` 获取临时安全凭证作为默认凭证。
### 5. 使用 ECS 实例RAM角色
### 自定义凭证提供程序链
可通过自定义程序链代替默认程序链的寻找顺序,也可以自行编写闭包传入提供者。
```php
<?php
若不存在优先级更高的凭据信息,Credentials工具将通过环境变量获取ALIBABA_CLOUD_ECS_METADATA(ECS实例RAM角色名称)的值。若该变量的值存在,程序将采用加固模式(IMDSv2)访问ECS的元数据服务(Meta Data Server),以获取ECS实例RAM角色的STS Token作为默认凭据信息。在使用加固模式时若发生异常,将使用普通模式兜底来获取访问凭据。您也可以通过设置环境变量ALIBABA_CLOUD_IMDSV1_DISABLED,执行不同的异常处理逻辑:
use AlibabaCloud\Credentials\Providers\ChainProvider;
- 当值为false时,会使用普通模式继续获取访问凭据。
ChainProvider::set(
ChainProvider::ini(),
ChainProvider::env(),
ChainProvider::instance()
);
- 当值为true时,表示只能使用加固模式获取访问凭据,会抛出异常。
服务端是否支持IMDSv2,取决于您在服务器的配置。
### 6. 使用外部服务 Credentials URI
若不存在优先级更高的凭据信息,Credentials工具会在环境变量中获取ALIBABA_CLOUD_CREDENTIALS_URI,若存在,程序将请求该URI地址,获取临时安全凭证作为默认凭据信息。
外部服务响应结构应如下:
```json
{
"Code": "Success",
"AccessKeyId": "AccessKeyId",
"AccessKeySecret": "AccessKeySecret",
"SecurityToken": "SecurityToken",
"Expiration": "2024-10-26T03:46:38Z"
}
```
## 文档
* [先决条件](/docs/zh-CN/0-Prerequisites.md)
* [安装](/docs/zh-CN/1-Installation.md)
## 问题
[提交 Issue](https://github.com/aliyun/credentials-php/issues/new/choose),不符合指南的问题可能会立即关闭。
## 发行说明
每个版本的详细更改记录在[发行说明](/CHANGELOG.md)中。
## 贡献
提交 Pull Request 之前请阅读[贡献指南](/CONTRIBUTING.md)。
## 相关
* [OpenAPI 开发者门户][open-api]
* [Packagist][packagist]
* [Composer][composer]
* [Guzzle中文文档][guzzle-docs]
* [最新源码][latest-release]
## 许可证
[Apache-2.0](/LICENSE.md)
Copyright (c) 2009-present, Alibaba Cloud All rights reserved.
[open-api]: https://next.api.aliyun.com
[open-api]: https://api.aliyun.com
[latest-release]: https://github.com/aliyun/credentials-php
[guzzle-docs]: https://guzzle-cn.readthedocs.io/zh_CN/latest/request-options.html
[composer]: https://getcomposer.org
[packagist]: https://packagist.org/packages/alibabacloud/credentials
[home]: https://home.console.aliyun.com
[aliyun]: https://www.aliyun.com
[cURL]: http://php.net/manual/zh/book.curl.php
[OPCache]: http://php.net/manual/zh/book.opcache.php
[xdebug]: http://xdebug.org
[OpenSSL]: http://php.net/manual/zh/book.openssl.php

View File

@@ -1,41 +1,59 @@
English | [简体中文](/README-zh-CN.md)
![](https://aliyunsdk-pages.alicdn.com/icons/AlibabaCloud.svg)
# Alibaba Cloud Credentials for PHP
[![PHP CI](https://github.com/aliyun/credentials-php/actions/workflows/ci.yml/badge.svg)](https://github.com/aliyun/credentials-php/actions/workflows/ci.yml)
[![codecov](https://codecov.io/gh/aliyun/credentials-php/graph/badge.svg?token=YIkSjtfKbB)](https://codecov.io/gh/aliyun/credentials-php)
[![Latest Stable Version](https://poser.pugx.org/alibabacloud/credentials/v/stable)](https://packagist.org/packages/alibabacloud/credentials)
[![composer.lock](https://poser.pugx.org/alibabacloud/credentials/composerlock)](https://packagist.org/packages/alibabacloud/credentials)
[![Total Downloads](https://poser.pugx.org/alibabacloud/credentials/downloads)](https://packagist.org/packages/alibabacloud/credentials)
[![License](https://poser.pugx.org/alibabacloud/credentials/license)](https://packagist.org/packages/alibabacloud/credentials)
[![codecov](https://codecov.io/gh/aliyun/credentials-php/branch/master/graph/badge.svg)](https://codecov.io/gh/aliyun/credentials-php)
[![Travis Build Status](https://travis-ci.org/aliyun/credentials-php.svg?branch=master)](https://travis-ci.org/aliyun/credentials-php)
[![Appveyor Build Status](https://ci.appveyor.com/api/projects/status/6jxpwmhyfipagtge/branch/master?svg=true)](https://ci.appveyor.com/project/aliyun/credentials-php)
![](https://aliyunsdk-pages.alicdn.com/icons/AlibabaCloud.svg)
Alibaba Cloud Credentials for PHP is a tool that helps PHP developers manage their credentials.
## Prerequisites
Your system needs to meet [Prerequisites](/docs/zh-CN/0-Prerequisites.md), including PHP> = 5.6. We strongly recommend using the cURL extension and compiling cURL 7.16.2+ using the TLS backend.
## Installation
If you have [Globally Install Composer](https://getcomposer.org/doc/00-intro.md#globally) on your system, install Alibaba Cloud Credentials for PHP as a dependency by running the following directly in the project directory:
```
```sh
composer require alibabacloud/credentials
```
> Some users may not be able to install due to network problems, you can switch to the [Alibaba Cloud Composer Mirror](https://developer.aliyun.com/composer).
See [Installation](/docs/zh-CN/1-Installation.md) for details on installing through Composer and other means.
## Quick Examples
Before you begin, you need to sign up for an Alibaba Cloud account and retrieve your [Credentials](https://usercenter.console.aliyun.com/#/manage/ak).
### Credential Type
#### Default credential provider chain
If you do not specify a method to initialize a Credentials client, the default credential provider chain is used. For more information, see the Default credential provider chain section of this topic.
```php
<?php
use AlibabaCloud\Credentials\Credential;
// Chain Provider if no Parameter
// Do not specify a method to initialize a Credentials client.
$client = new Credential();
$credential = $client->getCredential();
$credential->getAccessKeyId();
$credential->getAccessKeySecret();
$credential->getSecurityToken();
```
#### AccessKey
Setup access_key credential through [User Information Management][ak], it have full authority over the account, please keep it safe. Sometimes for security reasons, you cannot hand over a primary account AccessKey with full access to the developer of a project. You may create a sub-account [RAM Sub-account][ram] , grant its [authorization][permissions],and use the AccessKey of RAM Sub-account.
@@ -44,20 +62,19 @@ Setup access_key credential through [User Information Management][ak], it have f
<?php
use AlibabaCloud\Credentials\Credential;
// Chain Provider if no Parameter
$credential = new Credential();
$credential->getAccessKeyId();
$credential->getAccessKeySecret();
use AlibabaCloud\Credentials\Credential\Config;
// Access Key
$ak = new Credential([
$config = new Config([
'type' => 'access_key',
'access_key_id' => '<access_key_id>',
'access_key_secret' => '<access_key_secret>',
'accessKeyId' => '<access_key_id>',
'accessKeySecret' => '<access_key_secret>',
]);
$ak->getAccessKeyId();
$ak->getAccessKeySecret();
$client = new Credential($config);
$credential = $client->getCredential();
$credential->getAccessKeyId();
$credential->getAccessKeySecret();
```
#### STS
@@ -68,16 +85,20 @@ Create a temporary security credential by applying Temporary Security Credential
<?php
use AlibabaCloud\Credentials\Credential;
use AlibabaCloud\Credentials\Credential\Config;
$sts = new Credential([
$config = new Config([
'type' => 'sts',
'access_key_id' => '<access_key_id>',
'accessKey_secret' => '<accessKey_secret>',
'security_token' => '<security_token>',
'accessKeyId' => '<access_key_id>',
'accessKeySecret' => '<access_key_secret>',
'securityToken' => '<security_token>',
]);
$sts->getAccessKeyId();
$sts->getAccessKeySecret();
$sts->getSecurityToken();
$client = new Credential($config);
$credential = $client->getCredential();
$credential->getAccessKeyId();
$credential->getAccessKeySecret();
$credential->getSecurityToken();
```
#### RamRoleArn
@@ -88,56 +109,116 @@ By specifying [RAM Role][RAM Role], the credential will be able to automatically
<?php
use AlibabaCloud\Credentials\Credential;
use AlibabaCloud\Credentials\Credential\Config;
$ramRoleArn = new Credential([
$config = new Config([
'type' => 'ram_role_arn',
'access_key_id' => '<access_key_id>',
'access_key_secret' => '<access_key_secret>',
'role_arn' => '<role_arn>',
'role_session_name' => '<role_session_name>',
'accessKeyId' => '<access_key_id>',
'accessKeySecret' => '<access_key_secret>',
// Specify the ARN of the RAM role to be assumed. Example: acs:ram::123456789012****:role/adminrole.
'roleArn' => '<role_arn>',
// Specify the name of the role session.
'roleSessionName' => '<role_session_name>',
// Optional. Specify limited permissions for the RAM role. Example: {"Statement": [{"Action": ["*"],"Effect": "Allow","Resource": ["*"]}],"Version":"1"}.
'policy' => '',
// Optional. Specify the expiration of the session
'roleSessionExpiration' => 3600,
]);
$ramRoleArn->getAccessKeyId();
$ramRoleArn->getAccessKeySecret();
$ramRoleArn->getRoleArn();
$ramRoleArn->getRoleSessionName();
$ramRoleArn->getPolicy();
$client = new Credential($config);
$credential = $client->getCredential();
$credential->getAccessKeyId();
$credential->getAccessKeySecret();
$credential->getSecurityToken();
```
#### EcsRamRole
By specifying the role name, the credential will be able to automatically request maintenance of STS Token.
Both ECS and ECI instances support binding instance RAM roles. When the Credentials tool is used in an instance, the RAM role bound to the instance will be automatically obtained, and the STS Token of the RAM role will be obtained by accessing the metadata service to complete the initialization of the credential client.
The instance metadata server supports two access modes: hardened mode and normal mode. The Credentials tool uses hardened mode (IMDSv2) by default to obtain access credentials. If an exception occurs when using hardened mode, you can set disableIMDSv1 to perform different exception handling logic:
- When the value is false (default value), the normal mode will continue to be used to obtain access credentials.
- When the value is true, it means that only hardened mode can be used to obtain access credentials, and an exception will be thrown.
Whether the server supports IMDSv2 depends on your configuration on the server.
```php
<?php
use AlibabaCloud\Credentials\Credential;
use AlibabaCloud\Credentials\Credential\Config;
$ecsRamRole = new Credential([
$config = new Config([
'type' => 'ecs_ram_role',
'role_name' => '<role_name>',
// Optional. Specify the name of the RAM role of the ECS instance. If you do not specify this parameter, its value is automatically obtained. To reduce the number of requests, we recommend that you specify this parameter.
'roleName' => '<role_name>',
//Optional, whether to forcibly disable IMDSv1, that is, to use IMDSv2 hardening mode, which can be set by the environment variable ALIBABA_CLOUD_IMDSV1_DISABLED
'disableIMDSv1' => true,
]);
$ecsRamRole->getRoleName();
// Note: `role_name` is optional. It will be retrieved automatically if not set. It is highly recommended to set it up to reduce requests.
$client = new Credential($config);
$credential = $client->getCredential();
$credential->getAccessKeyId();
$credential->getAccessKeySecret();
$credential->getSecurityToken();
```
#### RsaKeyPair
By specifying the public key Id and the private key file, the credential will be able to automatically request maintenance of the AccessKey before sending the request. Only Japan station is supported.
#### OIDCRoleArn
After you attach a RAM role to a worker node in an Container Service for Kubernetes, applications in the pods on the worker node can use the metadata server to obtain an STS token the same way in which applications on ECS instances do. However, if an untrusted application is deployed on the worker node, such as an application that is submitted by your customer and whose code is unavailable to you, you may not want the application to use the metadata server to obtain an STS token of the RAM role attached to the worker node. To ensure the security of cloud resources and enable untrusted applications to securely obtain required STS tokens, you can use the RAM Roles for Service Accounts (RRSA) feature to grant minimum necessary permissions to an application. In this case, the ACK cluster creates a service account OpenID Connect (OIDC) token file, associates the token file with a pod, and then injects relevant environment variables into the pod. Then, the Credentials tool uses the environment variables to call the AssumeRoleWithOIDC operation of STS and obtains an STS token of the RAM role. For more information about the RRSA feature, see [Use RRSA to authorize different pods to access different cloud services](https://www.alibabacloud.com/help/en/ack/ack-managed-and-ack-dedicated/user-guide/use-rrsa-to-authorize-pods-to-access-different-cloud-services#task-2142941).
```php
<?php
use AlibabaCloud\Credentials\Credential;
use AlibabaCloud\Credentials\Credential\Config;
$rsaKeyPair = new Credential([
'type' => 'rsa_key_pair',
'public_key_id' => '<public_key_id>',
'private_key_file' => '<private_key_file>',
$config = new Config([
'type' => 'oidc_role_arn',
// Specify the ARN of the OIDC IdP by specifying the ALIBABA_CLOUD_OIDC_PROVIDER_ARN environment variable.
'oidcProviderArn' => '<oidc_provider_arn>',
// Specify the path of the OIDC token file by specifying the ALIBABA_CLOUD_OIDC_TOKEN_FILE environment variable.
'oidcTokenFilePath' => '<oidc_token_file_path>',
// Specify the ARN of the RAM role by specifying the ALIBABA_CLOUD_ROLE_ARN environment variable.
'roleArn' => '<role_arn>',
// Specify the role session name by specifying the ALIBABA_CLOUD_ROLE_SESSION_NAME environment variable.
'roleSessionName' => '<role_session_name>',
// Optional. Specify limited permissions for the RAM role. Example: {"Statement": [{"Action": ["*"],"Effect": "Allow","Resource": ["*"]}],"Version":"1"}.
'policy' => '',
// Optional. Specify the validity period of the session.
'roleSessionExpiration' => 3600,
]);
$rsaKeyPair->getPublicKeyId();
$rsaKeyPair->getPrivateKey();
$client = new Credential($config);
$credential = $client->getCredential();
$credential->getAccessKeyId();
$credential->getAccessKeySecret();
$credential->getSecurityToken();
```
#### Credentials URI
By specifying the url, the credential will be able to automatically request maintenance of STS Token.
```php
<?php
use AlibabaCloud\Credentials\Credential;
use AlibabaCloud\Credentials\Credential\Config;
$config = new Config([
'type' => 'credentials_uri',
// Format: http url. `credentialsURI` can be replaced by setting environment variable: ALIBABA_CLOUD_CREDENTIALS_URI
'credentialsURI' => '<credentials_uri>',
]);
$client = new Credential($config);
$credential = $client->getCredential();
$credential->getAccessKeyId();
$credential->getAccessKeySecret();
$credential->getSecurityToken();
```
#### Bearer Token
@@ -148,24 +229,110 @@ If credential is required by the Cloud Call Centre (CCC), please apply for Beare
<?php
use AlibabaCloud\Credentials\Credential;
use AlibabaCloud\Credentials\Credential\Config;
$bearerToken = new Credential([
'type' => 'bearer_token',
'bearer_token' => '<bearer_token>',
$config = new Config([
'type' => 'bearer',
'bearerToken' => '<bearer_token>',
]);
$bearerToken->getBearerToken();
$bearerToken->getSignature();
$client = new Credential($config);
$credential = $client->getCredential();
$credential->getBearerToken();
```
## Default credential provider chain
The default credential provider chain looks for available credentials, looking in the following order:
If you want to use different types of credentials in the development and production environments of your application, you generally need to obtain the environment information from the code and write code branches to obtain different credentials for the development and production environments. The default credential provider chain of the Credentials tool allows you to use the same code to obtain credentials for different environments based on configurations independent of the application. If you use $credential = new Credential(); to initialize a Credentials client without specifying an initialization method, the Credentials tool obtains the credential information in the following order:
### 1. Environmental certificate
The program first looks for environment credentials in the environment variable. If the `ALIBABA_CLOUD_ACCESS_KEY_ID` and `ALIBABA_CLOUD_ACCESS_KEY_SECRET` environment variables are defined and not empty, the program will use them to create default credentials.
### 2. Configuration file
> If the user's home directory has the default file `~/.alibabacloud/credentials` (Windows is `C:\Users\USER_NAME\.alibabacloud\credentials`), the program will automatically create credentials with the specified type and name. The default file may not exist, but parsing errors will throw an exception. The voucher name is not case sensitive. If the voucher has the same name, the latter will overwrite the former. This configuration file can be shared between different projects and tools, and it will not be accidentally submitted to version control because it is outside the project. Environment variables can be referenced to the home directory %UserProfile% on Windows. Unix-like systems can use the environment variable $HOME or ~ (tilde). The path to the default file can be modified by defining the `ALIBABA_CLOUD_CREDENTIALS_FILE` environment variable.
Look for environment credentials in environment variable.
- If the `ALIBABA_CLOUD_ACCESS_KEY_ID` and `ALIBABA_CLOUD_ACCESS_KEY_SECRET` environment variables are defined and are not empty, the program will use them to create default credentials.
- If the `ALIBABA_CLOUD_ACCESS_KEY_ID`, `ALIBABA_CLOUD_ACCESS_KEY_SECRET` and `ALIBABA_CLOUD_SECURITY_TOKEN` environment variables are defined and are not empty, the program will use them to create temporary security credentials(STS). Note: This token has an expiration time, it is recommended to use it in a temporary environment.
### 2. The RAM role of an OIDC IdP
If no credentials are found in the previous step, the Credentials tool obtains the values of the following environment variables:
`ALIBABA_CLOUD_ROLE_ARN`: the ARN of the RAM role.
`ALIBABA_CLOUD_OIDC_PROVIDER_ARN`: the ARN of the OIDC IdP.
`ALIBABA_CLOUD_OIDC_TOKEN_FILE`: the path of the OIDC token file.
If the preceding three environment variables are specified, the Credentials tool uses the environment variables to call the [AssumeRoleWithOIDC](https://www.alibabacloud.com/help/en/ram/developer-reference/api-sts-2015-04-01-assumerolewithoidc) operation of STS to obtain an STS token as the default credential.
### 3. Using the config.json Configuration File of Aliyun CLI Tool
If there is no higher-priority credential information, the Credentials tool will first check the following locations to see if the config.json file exists:
Linux system: `~/.aliyun/config.json`
Windows system: `C:\Users\USER_NAME\.aliyun\config.json`
If the file exists, the program will use the credential information specified by `current` in the configuration file to initialize the credentials client. Of course, you can also use the environment variable `ALIBABA_CLOUD_PROFILE` to specify the credential information, for example by setting the value of `ALIBABA_CLOUD_PROFILE` to `AK`.
In the config.json configuration file, the value of each module represents different ways to obtain credential information:
- AK: Use the Access Key of the user as credential information;
- RamRoleArn: Use the ARN of the RAM role to obtain credential information;
- EcsRamRole: Use the RAM role bound to the ECS to obtain credential information;
- OIDC: Obtain credential information through OIDC ARN and OIDC Token;
- ChainableRamRoleArn: Use the role chaining method to obtain new credential information by specifying other credentials in the JSON file.
The configuration example information is as follows:
```json
{
"current": "AK",
"profiles": [
{
"name": "AK",
"mode": "AK",
"access_key_id": "access_key_id",
"access_key_secret": "access_key_secret"
},
{
"name": "RamRoleArn",
"mode": "RamRoleArn",
"access_key_id": "access_key_id",
"access_key_secret": "access_key_secret",
"ram_role_arn": "ram_role_arn",
"ram_session_name": "ram_session_name",
"expired_seconds": 3600,
"sts_region": "cn-hangzhou"
},
{
"name": "EcsRamRole",
"mode": "EcsRamRole",
"ram_role_name": "ram_role_name"
},
{
"name": "OIDC",
"mode": "OIDC",
"ram_role_arn": "ram_role_arn",
"oidc_token_file": "path/to/oidc/file",
"oidc_provider_arn": "oidc_provider_arn",
"ram_session_name": "ram_session_name",
"expired_seconds": 3600,
"sts_region": "cn-hangzhou"
},
{
"name": "ChainableRamRoleArn",
"mode": "ChainableRamRoleArn",
"source_profile": "AK",
"ram_role_arn": "ram_role_arn",
"ram_session_name": "ram_session_name",
"expired_seconds": 3600,
"sts_region": "cn-hangzhou"
}
]
}
```
### 4. Configuration file
>
> If the user's home directory has the default file `~/.alibabacloud/credentials` (Windows is `C:\Users\USER_NAME\.alibabacloud\credentials`), the program will automatically create credentials with the specified type and name. You can also specify the configuration file path by configuring the `ALIBABA_CLOUD_CREDENTIALS_FILE` environment variable. If the configuration file exists, the application initializes a Credentials client by using the credential information that is specified by default in the configuration file. You can also configure the `ALIBABA_CLOUD_PROFILE` environment variable to modify the default credential information that is read.
Configuration example:
```ini
[default]
type = access_key # Authentication method is access_key
@@ -184,68 +351,72 @@ role_arn = role_arn
role_session_name = session_name
[project3]
type = rsa_key_pair # Authentication method is rsa_key_pair
public_key_id = publicKeyId # Public Key ID
private_key_file = /your/pk.pem # Private Key File
type=oidc_role_arn # Authentication method is oidc_role_arn
oidc_provider_arn=oidc_provider_arn
oidc_token_file_path=oidc_token_file_path
role_arn=role_arn
role_session_name=session_name
```
### 3. Instance RAM role
If the environment variable `ALIBABA_CLOUD_ECS_METADATA` is defined and not empty, the program will take the value of the environment variable as the role name and request `http://100.100.100.200/latest/meta-data/ram/security-credentials/` to get the temporary Security credentials are used as default credentials.
### 5. Instance RAM role
### Custom credential provider chain
You can replace the default order of the program chain by customizing the program chain, or you can write the closure to the provider.
```php
<?php
If there is no credential information with a higher priority, the Credentials tool will obtain the value of ALIBABA_CLOUD_ECS_METADATA (ECS instance RAM role name) through the environment variable. If the value of this variable exists, the program will use the hardened mode (IMDSv2) to access the metadata service (Meta Data Server) of ECS to obtain the STS Token of the ECS instance RAM role as the default credential information. If an exception occurs when using the hardened mode, the normal mode will be used as a fallback to obtain access credentials. You can also set the environment variable ALIBABA_CLOUD_IMDSV1_DISABLED to perform different exception handling logic:
use AlibabaCloud\Credentials\Providers\ChainProvider;
- When the value is false, the normal mode will continue to obtain access credentials.
ChainProvider::set(
ChainProvider::ini(),
ChainProvider::env(),
ChainProvider::instance()
);
- When the value is true, it means that only the hardened mode can be used to obtain access credentials, and an exception will be thrown.
Whether the server supports IMDSv2 depends on your configuration on the server.
### 6. Using External Service Credentials URI
If there are no higher-priority credential information, the Credentials tool will obtain the `ALIBABA_CLOUD_CREDENTIALS_URI` from the environment variables. If it exists, the program will request the URI address to obtain temporary security credentials as the default credential information.
The external service response structure should be as follows:
```json
{
"Code": "Success",
"AccessKeyId": "AccessKeyId",
"AccessKeySecret": "AccessKeySecret",
"SecurityToken": "SecurityToken",
"Expiration": "2024-10-26T03:46:38Z"
}
```
## Documentation
* [Prerequisites](/docs/zh-CN/0-Prerequisites.md)
* [Installation](/docs/zh-CN/1-Installation.md)
## Issue
[Submit Issue](https://github.com/aliyun/credentials-php/issues/new/choose), Problems that do not meet the guidelines may close immediately.
## Release notes
Detailed changes for each version are recorded in the [Release Notes](/CHANGELOG.md).
## Contribution
Please read the [Contribution Guide](/CONTRIBUTING.md) before submitting a Pull Request.
## Related
* [OpenAPI Developer Portal][open-api]
* [Packagist][packagist]
* [Composer][composer]
* [Guzzle Doc][guzzle-docs]
* [Latest Release][latest-release]
## License
[Apache-2.0](/LICENSE.md)
Copyright (c) 2009-present, Alibaba Cloud All rights reserved.
[open-api]: https://next.api.aliyun.com
[open-api]: https://api.alibabacloud.com
[latest-release]: https://github.com/aliyun/credentials-php
[guzzle-docs]: http://docs.guzzlephp.org/en/stable/request-options.html
[composer]: https://getcomposer.org
[packagist]: https://packagist.org/packages/alibabacloud/credentials
[home]: https://home.console.aliyun.com
[aliyun]: https://www.aliyun.com
[cURL]: https://www.php.net/manual/en/book.curl.php
[OPCache]: http://php.net/manual/en/book.opcache.php
[xdebug]: http://xdebug.org
[OpenSSL]: http://php.net/manual/en/book.openssl.php

View File

@@ -47,7 +47,7 @@
"ext-sockets": "*",
"drupal/coder": "^8.3",
"symfony/dotenv": "^3.4",
"phpunit/phpunit": "^4.8.35|^5.4.3",
"phpunit/phpunit": "^5.7|^6.6|^9.3",
"monolog/monolog": "^1.24",
"composer/composer": "^1.8",
"mikey179/vfsstream": "^1.6",
@@ -68,7 +68,10 @@
},
"config": {
"preferred-install": "dist",
"optimize-autoloader": true
"optimize-autoloader": true,
"allow-plugins": {
"dealerdirect/phpcodesniffer-composer-installer": true
}
},
"minimum-stability": "dev",
"prefer-stable": true,

View File

@@ -2,9 +2,12 @@
namespace AlibabaCloud\Credentials;
use AlibabaCloud\Credentials\Utils\Filter;
use AlibabaCloud\Credentials\Credential\CredentialModel;
use AlibabaCloud\Credentials\Signature\ShaHmac1Signature;
/**
* @deprecated
* Use the AccessKey to complete the authentication.
*/
class AccessKeyCredential implements CredentialsInterface
@@ -69,4 +72,15 @@ class AccessKeyCredential implements CredentialsInterface
{
return '';
}
/**
* @inheritDoc
*/
public function getCredential()
{
return new CredentialModel([
'accessKeyId' => $this->accessKeyId,
'accessKeySecret' => $this->accessKeySecret,
'type' => 'access_key',
]);
}
}

View File

@@ -2,6 +2,8 @@
namespace AlibabaCloud\Credentials;
use AlibabaCloud\Credentials\Utils\Filter;
use AlibabaCloud\Credentials\Credential\CredentialModel;
use AlibabaCloud\Credentials\Signature\BearerTokenSignature;
/**
@@ -18,13 +20,13 @@ class BearerTokenCredential implements CredentialsInterface
/**
* BearerTokenCredential constructor.
*
* @param $bearerToken
* @param $bearer_token
*/
public function __construct($bearerToken)
public function __construct($bearer_token)
{
Filter::bearerToken($bearerToken);
Filter::bearerToken($bearer_token);
$this->bearerToken = $bearerToken;
$this->bearerToken = $bearer_token;
}
/**
@@ -50,4 +52,16 @@ class BearerTokenCredential implements CredentialsInterface
{
return new BearerTokenSignature();
}
/**
* @inheritDoc
*/
public function getCredential()
{
return new CredentialModel([
'bearerToken' => $this->bearerToken,
'type' => 'bearer',
]);
}
}

View File

@@ -3,153 +3,188 @@
namespace AlibabaCloud\Credentials;
use AlibabaCloud\Credentials\Credential\Config;
use AlibabaCloud\Credentials\Credential\CredentialModel;
use AlibabaCloud\Credentials\Providers\DefaultCredentialsProvider;
use AlibabaCloud\Credentials\Providers\EcsRamRoleCredentialsProvider;
use AlibabaCloud\Credentials\Providers\OIDCRoleArnCredentialsProvider;
use AlibabaCloud\Credentials\Providers\RamRoleArnCredentialsProvider;
use AlibabaCloud\Credentials\Providers\RsaKeyPairCredentialsProvider;
use AlibabaCloud\Credentials\Providers\StaticAKCredentialsProvider;
use AlibabaCloud\Credentials\Providers\StaticSTSCredentialsProvider;
use AlibabaCloud\Credentials\Providers\URLCredentialsProvider;
use AlibabaCloud\Credentials\Utils\Helper;
use GuzzleHttp\Exception\GuzzleException;
use InvalidArgumentException;
use ReflectionClass;
use ReflectionException;
use ReflectionParameter;
use RuntimeException;
/**
* Class Credential
*
* @package AlibabaCloud\Credentials
*
* @mixin AccessKeyCredential
* @mixin BearerTokenCredential
* @mixin EcsRamRoleCredential
* @mixin RamRoleArnCredential
* @mixin RsaKeyPairCredential
*/
class Credential
{
/**
* @var array
*/
protected $config = [];
/**
* @var array
* Version of the Client
*/
protected $types = [
'access_key' => AccessKeyCredential::class,
'sts' => StsCredential::class,
'ecs_ram_role' => EcsRamRoleCredential::class,
'ram_role_arn' => RamRoleArnCredential::class,
'rsa_key_pair' => RsaKeyPairCredential::class,
];
const VERSION = '1.1.5';
/**
* @var AccessKeyCredential|BearerTokenCredential|EcsRamRoleCredential|RamRoleArnCredential|RsaKeyPairCredential
* @var Config
*/
protected $config;
/**
* @var CredentialsInterface
*/
protected $credential;
/**
* @var string
*/
protected $type;
/**
* Credential constructor.
*
* @param array|Config $config
*
* @throws ReflectionException
*/
public function __construct($config = [])
{
if ($config instanceof Config) {
$config = $this->parse($config);
}
if ($config !== []) {
$this->config = array_change_key_case($config);
$this->parseConfig();
if (\is_array($config)) {
if (empty($config)) {
$this->config = null;
} else {
$this->credential = Credentials::get()->getCredential();
$this->config = new Config($this->parseConfig($config));
}
} else {
$this->config = $config;
}
$this->credential = $this->getCredentials($this->config);
}
/**
* @param Config $config
* @param array $config
*
* @return array
*/
private function parse($config)
private function parseConfig($config)
{
$config = get_object_vars($config);
$res = [];
foreach ($config as $key => $value) {
$res[$this->toUnderScore($key)] = $value;
foreach (\array_change_key_case($config) as $key => $value) {
$res[Helper::snakeToCamelCase($key)] = $value;
}
return $res;
}
private function toUnderScore($str)
{
$dstr = preg_replace_callback('/([A-Z]+)/', function ($matchs) {
return '_' . strtolower($matchs[0]);
}, $str);
return trim(preg_replace('/_{2,}/', '_', $dstr), '_');
}
/**
* @throws ReflectionException
*/
private function parseConfig()
{
if (!isset($this->config['type'])) {
throw new InvalidArgumentException('Missing required type option');
}
$this->type = $this->config['type'];
if (!isset($this->types[$this->type])) {
throw new InvalidArgumentException(
'Invalid type option, support: ' .
implode(', ', array_keys($this->types))
);
}
$class = new ReflectionClass($this->types[$this->type]);
$parameters = [];
/**
* @var $parameter ReflectionParameter
*/
foreach ($class->getConstructor()->getParameters() as $parameter) {
$parameters[] = $this->getValue($parameter);
}
$this->credential = $class->newInstance(...$parameters);
}
/**
* @param ReflectionParameter $parameter
* Credentials getter.
*
* @param Config $config
* @return CredentialsInterface
*
* @return string|array
* @throws ReflectionException
*/
protected function getValue(ReflectionParameter $parameter)
private function getCredentials($config)
{
if ($parameter->name === 'config' || $parameter->name === 'credential') {
return $this->config;
if (is_null($config)) {
return new CredentialsProviderWrap('default', new DefaultCredentialsProvider());
}
foreach ($this->config as $key => $value) {
if (strtolower($parameter->name) === $key) {
return $value;
switch ($config->type) {
case 'access_key':
$provider = new StaticAKCredentialsProvider([
'accessKeyId' => $config->accessKeyId,
'accessKeySecret' => $config->accessKeySecret,
]);
return new CredentialsProviderWrap('access_key', $provider);
case 'sts':
$provider = new StaticSTSCredentialsProvider([
'accessKeyId' => $config->accessKeyId,
'accessKeySecret' => $config->accessKeySecret,
'securityToken' => $config->securityToken,
]);
return new CredentialsProviderWrap('sts', $provider);
case 'bearer':
return new BearerTokenCredential($config->bearerToken);
case 'ram_role_arn':
if (!is_null($config->securityToken) && $config->securityToken !== '') {
$innerProvider = new StaticSTSCredentialsProvider([
'accessKeyId' => $config->accessKeyId,
'accessKeySecret' => $config->accessKeySecret,
'securityToken' => $config->securityToken,
]);
} else {
$innerProvider = new StaticAKCredentialsProvider([
'accessKeyId' => $config->accessKeyId,
'accessKeySecret' => $config->accessKeySecret,
]);
}
$provider = new RamRoleArnCredentialsProvider([
'credentialsProvider' => $innerProvider,
'roleArn' => $config->roleArn,
'roleSessionName' => $config->roleSessionName,
'policy' => $config->policy,
'durationSeconds' => $config->roleSessionExpiration,
'externalId' => $config->externalId,
'stsEndpoint' => $config->STSEndpoint,
], [
'connectTimeout' => $config->connectTimeout,
'readTimeout' => $config->readTimeout,
]);
return new CredentialsProviderWrap('ram_role_arn', $provider);
case 'rsa_key_pair':
$provider = new RsaKeyPairCredentialsProvider([
'publicKeyId' => $config->publicKeyId,
'privateKeyFile' => $config->privateKeyFile,
'durationSeconds' => $config->roleSessionExpiration,
'stsEndpoint' => $config->STSEndpoint,
], [
'connectTimeout' => $config->connectTimeout,
'readTimeout' => $config->readTimeout,
]);
return new CredentialsProviderWrap('rsa_key_pair', $provider);
case 'ecs_ram_role':
$provider = new EcsRamRoleCredentialsProvider([
'roleName' => $config->roleName,
'disableIMDSv1' => $config->disableIMDSv1,
], [
'connectTimeout' => $config->connectTimeout,
'readTimeout' => $config->readTimeout,
]);
return new CredentialsProviderWrap('ecs_ram_role', $provider);
case 'oidc_role_arn':
$provider = new OIDCRoleArnCredentialsProvider([
'roleArn' => $config->roleArn,
'oidcProviderArn' => $config->oidcProviderArn,
'oidcTokenFilePath' => $config->oidcTokenFilePath,
'roleSessionName' => $config->roleSessionName,
'policy' => $config->policy,
'durationSeconds' => $config->roleSessionExpiration,
'stsEndpoint' => $config->STSEndpoint,
], [
'connectTimeout' => $config->connectTimeout,
'readTimeout' => $config->readTimeout,
]);
return new CredentialsProviderWrap('oidc_role_arn', $provider);
case "credentials_uri":
$provider = new URLCredentialsProvider([
'credentialsURI' => $config->credentialsURI,
], [
'connectTimeout' => $config->connectTimeout,
'readTimeout' => $config->readTimeout,
]);
return new CredentialsProviderWrap('credentials_uri', $provider);
default:
throw new InvalidArgumentException('Unsupported credential type option: ' . $config->type . ', support: access_key, sts, bearer, ecs_ram_role, ram_role_arn, rsa_key_pair, oidc_role_arn, credentials_uri');
}
if ($parameter->isDefaultValueAvailable()) {
return $parameter->getDefaultValue();
}
throw new InvalidArgumentException("Missing required {$parameter->name} option in config for {$this->type}");
}
/**
* @return AccessKeyCredential|BearerTokenCredential|EcsRamRoleCredential|RamRoleArnCredential|RsaKeyPairCredential
* @return CredentialModel
* @throws RuntimeException
* @throws GuzzleException
*/
public function getCredential()
{
return $this->credential;
return $this->credential->getCredential();
}
/**
@@ -157,17 +192,68 @@ class Credential
*/
public function getConfig()
{
return $this->config;
return $this->config->toMap();
}
/**
* @deprecated use getCredential() instead
*
* @return string
* @throws RuntimeException
* @throws GuzzleException
*/
public function getType()
{
return $this->type;
return $this->credential->getCredential()->getType();
}
/**
* @deprecated use getCredential() instead
*
* @return string
* @throws RuntimeException
* @throws GuzzleException
*/
public function getAccessKeyId()
{
return $this->credential->getCredential()->getAccessKeyId();
}
/**
* @deprecated use getCredential() instead
*
* @return string
* @throws RuntimeException
* @throws GuzzleException
*/
public function getAccessKeySecret()
{
return $this->credential->getCredential()->getAccessKeySecret();
}
/**
* @deprecated use getCredential() instead
*
* @return string
* @throws RuntimeException
* @throws GuzzleException
*/
public function getSecurityToken()
{
return $this->credential->getCredential()->getSecurityToken();
}
/**
* @deprecated use getCredential() instead
*
* @return string
* @throws RuntimeException
* @throws GuzzleException
*/
public function getBearerToken()
{
return $this->credential->getCredential()->getBearerToken();
}
/**
* @param string $name

View File

@@ -2,49 +2,269 @@
namespace AlibabaCloud\Credentials\Credential;
class Config
use AlibabaCloud\Tea\Model;
class Config extends Model
{
public function validate()
{
}
public function toMap()
{
$res = [];
if (null !== $this->accessKeyId) {
$res['accessKeyId'] = $this->accessKeyId;
}
if (null !== $this->accessKeySecret) {
$res['accessKeySecret'] = $this->accessKeySecret;
}
if (null !== $this->securityToken) {
$res['securityToken'] = $this->securityToken;
}
if (null !== $this->bearerToken) {
$res['bearerToken'] = $this->bearerToken;
}
if (null !== $this->durationSeconds) {
$res['durationSeconds'] = $this->durationSeconds;
}
if (null !== $this->roleArn) {
$res['roleArn'] = $this->roleArn;
}
if (null !== $this->policy) {
$res['policy'] = $this->policy;
}
if (null !== $this->roleSessionExpiration) {
$res['roleSessionExpiration'] = $this->roleSessionExpiration;
}
if (null !== $this->roleSessionName) {
$res['roleSessionName'] = $this->roleSessionName;
}
if (null !== $this->publicKeyId) {
$res['publicKeyId'] = $this->publicKeyId;
}
if (null !== $this->privateKeyFile) {
$res['privateKeyFile'] = $this->privateKeyFile;
}
if (null !== $this->roleName) {
$res['roleName'] = $this->roleName;
}
if (null !== $this->credentialsURI) {
$res['credentialsURI'] = $this->credentialsURI;
}
if (null !== $this->type) {
$res['type'] = $this->type;
}
if (null !== $this->STSEndpoint) {
$res['STSEndpoint'] = $this->STSEndpoint;
}
if (null !== $this->externalId) {
$res['externalId'] = $this->externalId;
}
return $res;
}
/**
* @param array $map
* @return Config
*/
public static function fromMap($map = [])
{
$model = new self();
if (isset($map['accessKeyId'])) {
$model->accessKeyId = $map['accessKeyId'];
}
if (isset($map['accessKeySecret'])) {
$model->accessKeySecret = $map['accessKeySecret'];
}
if (isset($map['securityToken'])) {
$model->securityToken = $map['securityToken'];
}
if (isset($map['bearerToken'])) {
$model->bearerToken = $map['bearerToken'];
}
if (isset($map['durationSeconds'])) {
$model->durationSeconds = $map['durationSeconds'];
}
if (isset($map['roleArn'])) {
$model->roleArn = $map['roleArn'];
}
if (isset($map['policy'])) {
$model->policy = $map['policy'];
}
if (isset($map['roleSessionExpiration'])) {
$model->roleSessionExpiration = $map['roleSessionExpiration'];
}
if (isset($map['roleSessionName'])) {
$model->roleSessionName = $map['roleSessionName'];
}
if (isset($map['publicKeyId'])) {
$model->publicKeyId = $map['publicKeyId'];
}
if (isset($map['privateKeyFile'])) {
$model->privateKeyFile = $map['privateKeyFile'];
}
if (isset($map['roleName'])) {
$model->roleName = $map['roleName'];
}
if (isset($map['credentialsURI'])) {
$model->credentialsURI = $map['credentialsURI'];
}
if (isset($map['type'])) {
$model->type = $map['type'];
}
if (isset($map['STSEndpoint'])) {
$model->STSEndpoint = $map['STSEndpoint'];
}
if (isset($map['externalId'])) {
$model->externalId = $map['externalId'];
}
return $model;
}
/**
* @description credential type
* @example access_key
* @var string
*/
public $type = 'default';
public $accessKeyId = "";
/**
* @description accesskey id
* @var string
*/
public $accessKeyId;
public $accessKeySecret = "";
/**
* @description accesskey secret
* @var string
*/
public $accessKeySecret;
public $securityToken = "";
/**
* @description security token
* @var string
*/
public $securityToken;
public $bearerToken = "";
/**
* @description bearer token
* @var string
*/
public $bearerToken;
public $roleName = "";
/**
* @description role name
* @var string
*/
public $roleName;
public $roleArn = "";
/**
* @description role arn
* @var string
*/
public $roleArn;
public $roleSessionName = "";
/**
* @description oidc provider arn
* @var string
*/
public $oidcProviderArn;
public $host = "";
/**
* @description oidc token file path
* @var string
*/
public $oidcTokenFilePath;
public $publicKeyId = "";
/**
* @description role session expiration
* @example 3600
* @var int
*/
public $roleSessionExpiration;
public $privateKeyFile = "";
/**
* @description role session name
* @var string
*/
public $roleSessionName;
public $readTimeout = 0;
/**
* @description role arn policy
* @var string
*/
public $policy;
public $connectTimeout = 0;
/**
* @description external id for ram role arn
* @var string
*/
public $externalId;
/**
* @description sts endpoint
* @var string
*/
public $STSEndpoint;
public $publicKeyId;
public $privateKeyFile;
/**
* @description read timeout
* @var int
*/
public $readTimeout;
/**
* @description connection timeout
* @var int
*/
public $connectTimeout;
/**
* @description disable IMDS v1
* @var bool
*/
public $disableIMDSv1;
/**
* @description credentials URI
* @var string
*/
public $credentialsURI;
/**
* @deprecated
*/
public $metadataTokenDuration;
/**
* @deprecated
*/
public $durationSeconds;
/**
* @deprecated
*/
public $host;
/**
* @deprecated
*/
public $expiration;
/**
* @deprecated
*/
public $certFile = "";
/**
* @deprecated
*/
public $certPassword = "";
public $proxy = "";
public $expiration = 0;
public function __construct($config)
{
foreach ($config as $k => $v) {
$this->{$k} = $v;
}
}
/**
* @internal
*/
public $proxy;
}

View File

@@ -0,0 +1,143 @@
<?php
// This file is auto-generated, don't edit it. Thanks.
namespace AlibabaCloud\Credentials\Credential;
use AlibabaCloud\Tea\Model;
class CredentialModel extends Model
{
public function validate()
{
}
public function toMap()
{
$res = [];
if (null !== $this->accessKeyId) {
$res['accessKeyId'] = $this->accessKeyId;
}
if (null !== $this->accessKeySecret) {
$res['accessKeySecret'] = $this->accessKeySecret;
}
if (null !== $this->securityToken) {
$res['securityToken'] = $this->securityToken;
}
if (null !== $this->bearerToken) {
$res['bearerToken'] = $this->bearerToken;
}
if (null !== $this->type) {
$res['type'] = $this->type;
}
if (null !== $this->providerName) {
$res['providerName'] = $this->providerName;
}
return $res;
}
/**
* @param array $map
* @return CredentialModel
*/
public static function fromMap($map = [])
{
$model = new self();
if (isset($map['accessKeyId'])) {
$model->accessKeyId = $map['accessKeyId'];
}
if (isset($map['accessKeySecret'])) {
$model->accessKeySecret = $map['accessKeySecret'];
}
if (isset($map['securityToken'])) {
$model->securityToken = $map['securityToken'];
}
if (isset($map['bearerToken'])) {
$model->bearerToken = $map['bearerToken'];
}
if (isset($map['type'])) {
$model->type = $map['type'];
}
if(isset($map['providerName'])){
$model->providerName = $map['providerName'];
}
return $model;
}
/**
* @description accesskey id
* @var string
*/
public $accessKeyId;
/**
* @description accesskey secret
* @var string
*/
public $accessKeySecret;
/**
* @description security token
* @var string
*/
public $securityToken;
/**
* @description bearer token
* @var string
*/
public $bearerToken;
/**
* @description type
* @example access_key
* @var string
*/
public $type;
/**
* @description provider name
* @example cli_profile/static_ak
* @var string
*/
public $providerName;
/**
* @return string
*/
public function getAccessKeyId()
{
return $this->accessKeyId;
}
/**
* @return string
*/
public function getAccessKeySecret()
{
return $this->accessKeySecret;
}
/**
* @return string
*/
public function getSecurityToken()
{
return $this->securityToken;
}
/**
* @return string
*/
public function getBearerToken()
{
return $this->bearerToken;
}
public function getType()
{
return $this->type;
}
public function getProviderName()
{
return $this->providerName;
}
}

View File

@@ -0,0 +1,97 @@
<?php
namespace AlibabaCloud\Credentials\Credential;
use AlibabaCloud\Credentials\Providers\Credentials;
class RefreshResult
{
/**
* RefreshResult constructor.
* @param Credentials $params
* @param int $staleTime
* @param int $prefetchTime
*/
public function __construct($credentials = null, $staleTime = PHP_INT_MAX, $prefetchTime = PHP_INT_MAX)
{
$this->credentials = $credentials;
$this->staleTime = $staleTime;
$this->prefetchTime = $prefetchTime;
}
public function validate() {}
public function toMap()
{
$res = [];
if (null !== $this->staleTime) {
$res['staleTime'] = $this->staleTime;
}
if (null !== $this->prefetchTime) {
$res['prefetchTime'] = $this->prefetchTime;
}
if (null !== $this->credentials) {
$res['credentials'] = $this->credentials;
}
return $res;
}
/**
* @param array $map
* @return RefreshResult
*/
public static function fromMap($map = [])
{
$model = new self();
if (isset($map['staleTime'])) {
$model->staleTime = $map['staleTime'];
}
if (isset($map['prefetchTime'])) {
$model->staleTime = $map['prefetchTime'];
}
if (isset($map['credentials'])) {
$model->staleTime = $map['credentials'];
}
return $model;
}
/**
* @description staleTime
* @var int
*/
public $staleTime;
/**
* @description prefetchTime
* @var int
*/
public $prefetchTime;
/**
* @description credentials
* @var Credentials
*/
public $credentials;
/**
* @return Credentials
*/
public function credentials()
{
return $this->credentials;
}
/**
* @var int
*/
public function staleTime()
{
return $this->staleTime;
}
/**
* @var int
*/
public function prefetchTime()
{
return $this->prefetchTime;
}
}

View File

@@ -3,6 +3,8 @@
namespace AlibabaCloud\Credentials;
use AlibabaCloud\Credentials\Providers\ChainProvider;
use AlibabaCloud\Credentials\Utils\Filter;
use AlibabaCloud\Credentials\Utils\MockTrait;
use ReflectionException;
use RuntimeException;

View File

@@ -2,9 +2,11 @@
namespace AlibabaCloud\Credentials;
use AlibabaCloud\Credentials\Credential\CredentialModel;
use AlibabaCloud\Credentials\Signature\SignatureInterface;
/**
* @internal This class is intended for internal use within the package.
* Interface CredentialsInterface
*
* @codeCoverageIgnore
@@ -12,12 +14,19 @@ use AlibabaCloud\Credentials\Signature\SignatureInterface;
interface CredentialsInterface
{
/**
* @deprecated
* @return string
*/
public function __toString();
/**
* @deprecated
* @return SignatureInterface
*/
public function getSignature();
/**
* @return CredentialModel
*/
public function getCredential();
}

View File

@@ -0,0 +1,76 @@
<?php
namespace AlibabaCloud\Credentials;
use AlibabaCloud\Credentials\Credential\CredentialModel;
use AlibabaCloud\Credentials\Providers\CredentialsProvider;
/**
* @internal This class is intended for internal use within the package.
* Class CredentialsProviderWrap
*
* @package AlibabaCloud\Credentials
*/
class CredentialsProviderWrap implements CredentialsInterface
{
/**
* @var string
*/
private $typeName;
/**
* @var CredentialsProvider
*/
private $credentialsProvider;
/**
* CLIProfileCredentialsProvider constructor.
*
* @param string $typeName
* @param CredentialsProvider $credentialsProvider
*/
public function __construct($typeName, $credentialsProvider)
{
$this->typeName = $typeName;
$this->credentialsProvider = $credentialsProvider;
}
/**
* @inheritDoc
*/
public function getCredential()
{
$credentials = $this->credentialsProvider->getCredentials();
return new CredentialModel([
'accessKeyId' => $credentials->getAccessKeyId(),
'accessKeySecret' => $credentials->getAccessKeySecret(),
'securityToken' => $credentials->getSecurityToken(),
'type' => $this->typeName,
'providerName' => $credentials->getProviderName(),
]);
}
/**
* @param string $name
* @param array $arguments
*
* @return mixed
*/
public function __call($name, $arguments)
{
return $this->credentialsProvider->$name($arguments);
}
public function __toString()
{
return "credentialsProviderWrap#$this->typeName";
}
/**
* @return ShaHmac1Signature
*/
public function getSignature()
{
return null;
}
}

View File

@@ -2,15 +2,18 @@
namespace AlibabaCloud\Credentials;
use AlibabaCloud\Credentials\Providers\EcsRamRoleProvider;
use AlibabaCloud\Credentials\Request\Request;
use AlibabaCloud\Credentials\Providers\EcsRamRoleCredentialsProvider;
use AlibabaCloud\Credentials\Credential\CredentialModel;
use AlibabaCloud\Credentials\Signature\ShaHmac1Signature;
use AlibabaCloud\Credentials\Request\Request;
use AlibabaCloud\Credentials\Utils\Filter;
use Exception;
use GuzzleHttp\Exception\GuzzleException;
use InvalidArgumentException;
use RuntimeException;
/**
* @deprecated
* Use the RAM role of an ECS instance to complete the authentication.
*/
class EcsRamRoleCredential implements CredentialsInterface
@@ -21,16 +24,33 @@ class EcsRamRoleCredential implements CredentialsInterface
*/
private $roleName;
/**
* @var boolean
*/
private $disableIMDSv1;
/**
* @var int
*/
private $metadataTokenDuration;
/**
* EcsRamRoleCredential constructor.
*
* @param $role_name
*/
public function __construct($role_name = null)
public function __construct($role_name = null, $disable_imdsv1 = false, $metadata_token_duration = 21600)
{
Filter::roleName($role_name);
$this->roleName = $role_name;
Filter::disableIMDSv1($disable_imdsv1);
$this->disableIMDSv1 = $disable_imdsv1;
$this->metadataTokenDuration = $metadata_token_duration;
}
/**
@@ -75,7 +95,7 @@ class EcsRamRoleCredential implements CredentialsInterface
throw new RuntimeException('Error retrieving credentials from result: ' . $result->getBody());
}
$role_name = (string)$result;
$role_name = (string) $result;
if (!$role_name) {
throw new RuntimeException('Error retrieving credentials from result is empty');
}
@@ -110,13 +130,18 @@ class EcsRamRoleCredential implements CredentialsInterface
}
/**
* @return StsCredential
* @return AlibabaCloud\Credentials\Providers\Credentials
* @throws Exception
* @throws GuzzleException
*/
protected function getSessionCredential()
{
return (new EcsRamRoleProvider($this))->get();
$params = [
"roleName" => $this->roleName,
'disableIMDSv1' => $this->disableIMDSv1,
'metadataTokenDuration' => $this->metadataTokenDuration,
];
return (new EcsRamRoleCredentialsProvider($params))->getCredentials();
}
/**
@@ -148,4 +173,27 @@ class EcsRamRoleCredential implements CredentialsInterface
{
return $this->getSessionCredential()->getExpiration();
}
/**
* @return bool
*/
public function isDisableIMDSv1()
{
return $this->disableIMDSv1;
}
/**
* @inheritDoc
*/
public function getCredential()
{
$credentials = $this->getSessionCredential();
return new CredentialModel([
'accessKeyId' => $credentials->getAccessKeyId(),
'accessKeySecret' => $credentials->getAccessKeySecret(),
'securityToken' => $credentials->getSecurityToken(),
'type' => 'ecs_ram_role',
]);
}
}

View File

@@ -1,134 +0,0 @@
<?php
namespace AlibabaCloud\Credentials;
use InvalidArgumentException;
class Filter
{
/**
* @param $name
*
* @codeCoverageIgnore
* @return string
*/
public static function credentialName($name)
{
if (!is_string($name)) {
throw new InvalidArgumentException('Name must be a string');
}
if ($name === '') {
throw new InvalidArgumentException('Name cannot be empty');
}
return $name;
}
/**
* @param $bearerToken
*
* @return mixed
* @throws InvalidArgumentException
*/
public static function bearerToken($bearerToken)
{
if (!is_string($bearerToken)) {
throw new InvalidArgumentException('Bearer Token must be a string');
}
if ($bearerToken === '') {
throw new InvalidArgumentException('Bearer Token cannot be empty');
}
return $bearerToken;
}
/**
* @param $publicKeyId
*
* @return mixed
*/
public static function publicKeyId($publicKeyId)
{
if (!is_string($publicKeyId)) {
throw new InvalidArgumentException('public_key_id must be a string');
}
if ($publicKeyId === '') {
throw new InvalidArgumentException('public_key_id cannot be empty');
}
return $publicKeyId;
}
/**
* @param $privateKeyFile
*
* @return mixed
*/
public static function privateKeyFile($privateKeyFile)
{
if (!is_string($privateKeyFile)) {
throw new InvalidArgumentException('private_key_file must be a string');
}
if ($privateKeyFile === '') {
throw new InvalidArgumentException('private_key_file cannot be empty');
}
return $privateKeyFile;
}
/**
* @param string|null $role_name
*/
public static function roleName($role_name)
{
if ($role_name === null) {
return;
}
if (!is_string($role_name)) {
throw new InvalidArgumentException('role_name must be a string');
}
if ($role_name === '') {
throw new InvalidArgumentException('role_name cannot be empty');
}
}
/**
* @param string $accessKeyId
* @param string $accessKeySecret
*/
public static function accessKey($accessKeyId, $accessKeySecret)
{
if (!is_string($accessKeyId)) {
throw new InvalidArgumentException('access_key_id must be a string');
}
if ($accessKeyId === '') {
throw new InvalidArgumentException('access_key_id cannot be empty');
}
if (!is_string($accessKeySecret)) {
throw new InvalidArgumentException('access_key_secret must be a string');
}
if ($accessKeySecret === '') {
throw new InvalidArgumentException('access_key_secret cannot be empty');
}
}
/**
* @param int $expiration
*/
public static function expiration($expiration)
{
if (!is_int($expiration)) {
throw new InvalidArgumentException('expiration must be a int');
}
}
}

View File

@@ -0,0 +1,193 @@
<?php
namespace AlibabaCloud\Credentials\Providers;
use AlibabaCloud\Credentials\Utils\Helper;
use RuntimeException;
/**
* @internal This class is intended for internal use within the package.
* Class CLIProfileCredentialsProvider
*
* @package AlibabaCloud\Credentials\Providers
*/
class CLIProfileCredentialsProvider implements CredentialsProvider
{
/**
* @var string
*/
private $profileName;
/**
* @var CredentialsProvider
*/
private $credentialsProvider;
/**
* CLIProfileCredentialsProvider constructor.
*
* @param array $params
*/
public function __construct(array $params = [])
{
$this->filterProfileName($params);
}
private function filterProfileName(array $params)
{
if (Helper::envNotEmpty('ALIBABA_CLOUD_PROFILE')) {
$this->profileName = Helper::env('ALIBABA_CLOUD_PROFILE');
}
if (isset($params['profileName'])) {
$this->profileName = $params['profileName'];
}
}
/**
* @return bool
*/
private function shouldReloadCredentialsProvider()
{
if (is_null($this->credentialsProvider)) {
return true;
}
return false;
}
/**
* @return CredentialsProvider
*/
protected function reloadCredentialsProvider($profileFile, $profileName)
{
if (!Helper::inOpenBasedir($profileFile)) {
throw new RuntimeException('Unable to open credentials file: ' . $profileFile);
}
if (!\is_readable($profileFile) || !\is_file($profileFile)) {
throw new RuntimeException('Credentials file is not readable: ' . $profileFile);
}
$jsonContent = \file_get_contents($profileFile);
$fileArray = json_decode($jsonContent, true);
if (\is_array($fileArray) && !empty($fileArray)) {
if (is_null($profileName) || $profileName === '') {
$profileName = $fileArray['current'];
}
if (isset($fileArray['profiles'])) {
foreach ($fileArray['profiles'] as $profile) {
if (Helper::unsetReturnNull($profile, 'name') === $profileName) {
switch (Helper::unsetReturnNull($profile, 'mode')) {
case 'AK':
return new StaticAKCredentialsProvider([
'accessKeyId' => Helper::unsetReturnNull($profile, 'access_key_id'),
'accessKeySecret' => Helper::unsetReturnNull($profile, 'access_key_secret'),
]);
case 'StsToken':
return new StaticSTSCredentialsProvider([
'accessKeyId' => Helper::unsetReturnNull($profile, 'access_key_id'),
'accessKeySecret' => Helper::unsetReturnNull($profile, 'access_key_secret'),
'securityToken' => Helper::unsetReturnNull($profile, 'sts_token'),
]);
case 'RamRoleArn':
$innerProvider = new StaticAKCredentialsProvider([
'accessKeyId' => Helper::unsetReturnNull($profile, 'access_key_id'),
'accessKeySecret' => Helper::unsetReturnNull($profile, 'access_key_secret'),
]);
return new RamRoleArnCredentialsProvider([
'credentialsProvider' => $innerProvider,
'roleArn' => Helper::unsetReturnNull($profile, 'ram_role_arn'),
'roleSessionName' => Helper::unsetReturnNull($profile, 'ram_session_name'),
'durationSeconds' => Helper::unsetReturnNull($profile, 'expired_seconds'),
'policy' => Helper::unsetReturnNull($profile, 'policy'),
'externalId' => Helper::unsetReturnNull($profile, 'external_id'),
'stsRegionId' => Helper::unsetReturnNull($profile, 'sts_region'),
'enableVpc' => Helper::unsetReturnNull($profile, 'enable_vpc'),
]);
case 'EcsRamRole':
return new EcsRamRoleCredentialsProvider([
'roleName' => Helper::unsetReturnNull($profile, 'ram_role_name'),
]);
case 'OIDC':
return new OIDCRoleArnCredentialsProvider([
'roleArn' => Helper::unsetReturnNull($profile, 'ram_role_arn'),
'oidcProviderArn' => Helper::unsetReturnNull($profile, 'oidc_provider_arn'),
'oidcTokenFilePath' => Helper::unsetReturnNull($profile, 'oidc_token_file'),
'roleSessionName' => Helper::unsetReturnNull($profile, 'ram_session_name'),
'durationSeconds' => Helper::unsetReturnNull($profile, 'expired_seconds'),
'policy' => Helper::unsetReturnNull($profile, 'policy'),
'stsRegionId' => Helper::unsetReturnNull($profile, 'sts_region'),
'enableVpc' => Helper::unsetReturnNull($profile, 'enable_vpc'),
]);
case 'ChainableRamRoleArn':
$previousProvider = $this->reloadCredentialsProvider($profileFile, Helper::unsetReturnNull($profile, 'source_profile'));
return new RamRoleArnCredentialsProvider([
'credentialsProvider' => $previousProvider,
'roleArn' => Helper::unsetReturnNull($profile, 'ram_role_arn'),
'roleSessionName' => Helper::unsetReturnNull($profile, 'ram_session_name'),
'durationSeconds' => Helper::unsetReturnNull($profile, 'expired_seconds'),
'policy' => Helper::unsetReturnNull($profile, 'policy'),
'externalId' => Helper::unsetReturnNull($profile, 'external_id'),
'stsRegionId' => Helper::unsetReturnNull($profile, 'sts_region'),
'enableVpc' => Helper::unsetReturnNull($profile, 'enable_vpc'),
]);
default:
throw new RuntimeException('Unsupported credential mode from CLI credentials file: ' . Helper::unsetReturnNull($profile, 'mode'));
}
}
}
}
}
throw new RuntimeException('Failed to get credential from CLI credentials file: ' . $profileFile);
}
/**
* Get credential.
*
* @return Credentials
* @throws RuntimeException
*/
public function getCredentials()
{
if (Helper::envNotEmpty('ALIBABA_CLOUD_CLI_PROFILE_DISABLED') && Helper::env('ALIBABA_CLOUD_CLI_PROFILE_DISABLED') === true) {
throw new RuntimeException('CLI credentials file is disabled');
}
$cliProfileFile = self::getDefaultFile();
if ($this->shouldReloadCredentialsProvider()) {
$this->credentialsProvider = $this->reloadCredentialsProvider($cliProfileFile, $this->profileName);
}
$credentials = $this->credentialsProvider->getCredentials();
return new Credentials([
'accessKeyId' => $credentials->getAccessKeyId(),
'accessKeySecret' => $credentials->getAccessKeySecret(),
'securityToken' => $credentials->getSecurityToken(),
'providerName' => $this->getProviderName() . '/' . $this->credentialsProvider->getProviderName(),
]);
}
/**
* Get the default credential file.
*
* @return string
*/
private function getDefaultFile()
{
return Helper::getHomeDirectory() .
DIRECTORY_SEPARATOR .
'.aliyun' .
DIRECTORY_SEPARATOR .
'config.json';
}
/**
* @return string
*/
public function getProviderName()
{
return 'cli_profile';
}
}

View File

@@ -3,12 +3,13 @@
namespace AlibabaCloud\Credentials\Providers;
use AlibabaCloud\Credentials\Credentials;
use AlibabaCloud\Credentials\Helper;
use AlibabaCloud\Credentials\Utils\Helper;
use Closure;
use InvalidArgumentException;
use RuntimeException;
/**
* @deprecated
* Class ChainProvider
*
* @package AlibabaCloud\Credentials\Providers

View File

@@ -0,0 +1,87 @@
<?php
namespace AlibabaCloud\Credentials\Providers;
/**
* @internal This class is intended for internal use within the package.
* Class Credentials
*
* @package AlibabaCloud\Credentials\Providers
*/
class Credentials
{
/**
* @var string
*/
private $accessKeyId;
/**
* @var string
*/
private $accessKeySecret;
/**
* @var string
*/
private $securityToken;
/**
* @var int
*/
private $expiration;
/**
* @var int
*/
private $providerName;
public function __construct($config = [])
{
if (!empty($config)) {
foreach ($config as $k => $v) {
$this->{$k} = $v;
}
}
}
/**
* @return string
*/
public function getAccessKeyId()
{
return $this->accessKeyId;
}
/**
* @return string
*/
public function getAccessKeySecret()
{
return $this->accessKeySecret;
}
/**
* @return string
*/
public function getSecurityToken()
{
return $this->securityToken;
}
/**
* @return int
*/
public function getExpiration()
{
return $this->expiration;
}
/**
* @return string
*/
public function getProviderName()
{
return $this->providerName;
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace AlibabaCloud\Credentials\Providers;
/**
* @internal This class is intended for internal use within the package.
* Interface CredentialsInterface
*
* @codeCoverageIgnore
*/
interface CredentialsProvider
{
/**
* @return Credentials
*/
public function getCredentials();
/**
* @return string
*/
public function getProviderName();
}

View File

@@ -0,0 +1,175 @@
<?php
namespace AlibabaCloud\Credentials\Providers;
use AlibabaCloud\Credentials\Utils\Filter;
use AlibabaCloud\Credentials\Utils\Helper;
use InvalidArgumentException;
use RuntimeException;
use Exception;
/**
* @internal This class is intended for internal use within the package.
* Class DefaultCredentialsProvider
*
* @package AlibabaCloud\Credentials\Providers
*/
class DefaultCredentialsProvider implements CredentialsProvider
{
/**
* @var array
*/
private static $defaultProviders = [];
/**
* @var bool
*/
private $reuseLastProviderEnabled;
/**
* @var CredentialsProvider
*/
private $lastUsedCredentialsProvider;
/**
* @var array
*/
private static $customChain = [];
/**
* DefaultCredentialsProvider constructor.
* @param array $params
*/
public function __construct(array $params = [])
{
$this->filterReuseLastProviderEnabled($params);
$this->createDefaultChain();
Filter::reuseLastProviderEnabled($this->reuseLastProviderEnabled);
}
private function filterReuseLastProviderEnabled(array $params)
{
$this->reuseLastProviderEnabled = true;
if (isset($params['reuseLastProviderEnabled'])) {
$this->reuseLastProviderEnabled = $params['reuseLastProviderEnabled'];
}
}
private function createDefaultChain()
{
self::$defaultProviders = [
new EnvironmentVariableCredentialsProvider(),
];
if (
Helper::envNotEmpty('ALIBABA_CLOUD_ROLE_ARN')
&& Helper::envNotEmpty('ALIBABA_CLOUD_OIDC_PROVIDER_ARN')
&& Helper::envNotEmpty('ALIBABA_CLOUD_OIDC_TOKEN_FILE')
) {
array_push(
self::$defaultProviders,
new OIDCRoleArnCredentialsProvider()
);
}
array_push(
self::$defaultProviders,
new CLIProfileCredentialsProvider()
);
array_push(
self::$defaultProviders,
new ProfileCredentialsProvider()
);
array_push(
self::$defaultProviders,
new EcsRamRoleCredentialsProvider()
);
if (Helper::envNotEmpty('ALIBABA_CLOUD_CREDENTIALS_URI')) {
array_push(
self::$defaultProviders,
new URLCredentialsProvider()
);
}
}
/**
* @param CredentialsProvider ...$providers
*/
public static function set(...$providers)
{
if (empty($providers)) {
throw new InvalidArgumentException('No providers in chain');
}
foreach ($providers as $provider) {
if (!$provider instanceof CredentialsProvider) {
throw new InvalidArgumentException('Providers must all be CredentialsProvider');
}
}
self::$customChain = $providers;
}
/**
* @return bool
*/
public static function hasCustomChain()
{
return (bool) self::$customChain;
}
public static function flush()
{
self::$customChain = [];
}
/**
* Get credential.
*
* @return Credentials
* @throws RuntimeException
*/
public function getCredentials()
{
if ($this->reuseLastProviderEnabled && !is_null($this->lastUsedCredentialsProvider)) {
$credentials = $this->lastUsedCredentialsProvider->getCredentials();
return new Credentials([
'accessKeyId' => $credentials->getAccessKeyId(),
'accessKeySecret' => $credentials->getAccessKeySecret(),
'securityToken' => $credentials->getSecurityToken(),
'providerName' => $this->getProviderName() . '/' . $this->lastUsedCredentialsProvider->getProviderName(),
]);
}
$providerChain = array_merge(
self::$customChain,
self::$defaultProviders
);
$exceptionMessages = [];
foreach ($providerChain as $provider) {
try {
$credentials = $provider->getCredentials();
$this->lastUsedCredentialsProvider = $provider;
return new Credentials([
'accessKeyId' => $credentials->getAccessKeyId(),
'accessKeySecret' => $credentials->getAccessKeySecret(),
'securityToken' => $credentials->getSecurityToken(),
'providerName' => $this->getProviderName() . '/' . $provider->getProviderName(),
]);
} catch (Exception $exception) {
array_push($exceptionMessages, basename(str_replace('\\', '/', get_class($provider))) . ': ' . $exception->getMessage());
}
}
throw new RuntimeException('Unable to load credentials from any of the providers in the chain: ' . implode(', ', $exceptionMessages));
}
/**
* @inheritDoc
*/
public function getProviderName()
{
return "default";
}
}

View File

@@ -0,0 +1,276 @@
<?php
namespace AlibabaCloud\Credentials\Providers;
use AlibabaCloud\Credentials\Utils\Helper;
use AlibabaCloud\Credentials\Utils\Filter;
use AlibabaCloud\Credentials\Request\Request;
use GuzzleHttp\Exception\GuzzleException;
use InvalidArgumentException;
use RuntimeException;
use AlibabaCloud\Credentials\Credential\RefreshResult;
/**
* @internal This class is intended for internal use within the package.
* Class EcsRamRoleCredentialsProvider
*
* @package AlibabaCloud\Credentials\Providers
*/
class EcsRamRoleCredentialsProvider extends SessionCredentialsProvider
{
/**
* @var string
*/
private $metadataHost = 'http://100.100.100.200';
/**
* @var string
*/
private $ecsUri = '/latest/meta-data/ram/security-credentials/';
/**
* @var string
*/
private $metadataTokenUri = '/latest/api/token';
/**
* @var string
*/
private $roleName;
/**
* @var boolean
*/
private $disableIMDSv1 = false;
/**
* @var int
*/
private $metadataTokenDuration = 21600;
/**
* @var int
*/
private $connectTimeout = 1;
/**
* @var int
*/
private $readTimeout = 1;
/**
* EcsRamRoleCredentialsProvider constructor.
*
* @param array $params
* @param array $options
*/
public function __construct(array $params = [], array $options = [])
{
$this->filterOptions($options);
$this->filterRoleName($params);
$this->filterDisableECSIMDSv1($params);
Filter::roleName($this->roleName);
Filter::disableIMDSv1($this->disableIMDSv1);
}
private function filterOptions(array $options)
{
if (isset($options['connectTimeout'])) {
$this->connectTimeout = $options['connectTimeout'];
}
if (isset($options['readTimeout'])) {
$this->readTimeout = $options['readTimeout'];
}
Filter::timeout($this->connectTimeout, $this->readTimeout);
}
private function filterRoleName(array $params)
{
if (Helper::envNotEmpty('ALIBABA_CLOUD_ECS_METADATA')) {
$this->roleName = Helper::env('ALIBABA_CLOUD_ECS_METADATA');
}
if (isset($params['roleName'])) {
$this->roleName = $params['roleName'];
}
}
private function filterDisableECSIMDSv1($params)
{
if (Helper::envNotEmpty('ALIBABA_CLOUD_IMDSV1_DISABLED')) {
$this->disableIMDSv1 = Helper::env('ALIBABA_CLOUD_IMDSV1_DISABLED') === true ? true : false;
}
if (isset($params['disableIMDSv1'])) {
$this->disableIMDSv1 = $params['disableIMDSv1'];
}
}
/**
* Get credentials by request.
*
* @return RefreshResult
* @throws InvalidArgumentException
* @throws RuntimeException
* @throws GuzzleException
*/
public function refreshCredentials()
{
if (Helper::envNotEmpty('ALIBABA_CLOUD_ECS_METADATA_DISABLED') && Helper::env('ALIBABA_CLOUD_ECS_METADATA_DISABLED') === true) {
throw new RuntimeException('IMDS credentials is disabled');
}
if (is_null($this->roleName) || $this->roleName === '') {
$this->roleName = $this->getRoleNameFromMeta();
}
$url = $this->metadataHost . $this->ecsUri . $this->roleName;
$options = Request::commonOptions();
$options['read_timeout'] = $this->readTimeout;
$options['connect_timeout'] = $this->connectTimeout;
$metadataToken = $this->getMetadataToken();
if (!is_null($metadataToken)) {
$options['headers']['X-aliyun-ecs-metadata-token'] = $metadataToken;
}
$result = Request::createClient()->request('GET', $url, $options);
if ($result->getStatusCode() === 404) {
throw new InvalidArgumentException('The role was not found in the instance' . (string) $result);
}
if ($result->getStatusCode() !== 200) {
throw new RuntimeException('Error refreshing credentials from IMDS, statusCode: ' . $result->getStatusCode() . ', result: ' . (string) $result);
}
$credentials = $result->toArray();
if (!isset($credentials['AccessKeyId']) || !isset($credentials['AccessKeySecret']) || !isset($credentials['SecurityToken'])) {
throw new RuntimeException('Error retrieving credentials from IMDS result:' . $result->toJson());
}
if (!isset($credentials['Code']) || $credentials['Code'] !== 'Success') {
throw new RuntimeException('Error retrieving credentials from IMDS result, Code is not Success:' . $result->toJson());
}
return new RefreshResult(new Credentials([
'accessKeyId' => $credentials['AccessKeyId'],
'accessKeySecret' => $credentials['AccessKeySecret'],
'securityToken' => $credentials['SecurityToken'],
'expiration' => \strtotime($credentials['Expiration']),
'providerName' => $this->getProviderName(),
]), $this->getStaleTime(strtotime($credentials["Expiration"])), $this->getPrefetchTime(strtotime($credentials["Expiration"])));
}
/**
* @return string
* @throws InvalidArgumentException
* @throws RuntimeException
* @throws GuzzleException
*/
private function getRoleNameFromMeta()
{
$options = Request::commonOptions();
$options['read_timeout'] = $this->readTimeout;
$options['connect_timeout'] = $this->connectTimeout;
$metadataToken = $this->getMetadataToken();
if (!is_null($metadataToken)) {
$options['headers']['X-aliyun-ecs-metadata-token'] = $metadataToken;
}
$result = Request::createClient()->request(
'GET',
'http://100.100.100.200/latest/meta-data/ram/security-credentials/',
$options
);
if ($result->getStatusCode() === 404) {
throw new InvalidArgumentException('The role name was not found in the instance' . (string) $result);
}
if ($result->getStatusCode() !== 200) {
throw new RuntimeException('Error retrieving role name from result: ' . (string) $result);
}
$role_name = (string) $result;
if (!$role_name) {
throw new RuntimeException('Error retrieving role name from result is empty');
}
return $role_name;
}
/**
* Get metadata token by request.
*
* @return string
* @throws RuntimeException
* @throws GuzzleException
*/
private function getMetadataToken()
{
$url = $this->metadataHost . $this->metadataTokenUri;
$options = Request::commonOptions();
$options['read_timeout'] = $this->readTimeout;
$options['connect_timeout'] = $this->connectTimeout;
$options['headers']['X-aliyun-ecs-metadata-token-ttl-seconds'] = $this->metadataTokenDuration;
$result = Request::createClient()->request('PUT', $url, $options);
if ($result->getStatusCode() != 200) {
if ($this->disableIMDSv1) {
throw new RuntimeException('Failed to get token from ECS Metadata Service. HttpCode= ' . $result->getStatusCode());
}
return null;
}
return (string) $result;
}
/**
* @var int
*/
public function getPrefetchTime($expiration)
{
return $expiration <= 0 ?
time() + (5 * 60) :
time() + (60 * 60);
}
/**
* @return string
*/
public function key()
{
return 'ecs_ram_role#roleName#' . $this->roleName;
}
/**
* @return string
*/
public function getProviderName()
{
return 'ecs_ram_role';
}
/**
* @return string
*/
public function getRoleName()
{
return $this->roleName;
}
/**
* @return bool
*/
public function isDisableIMDSv1()
{
return $this->disableIMDSv1;
}
}

View File

@@ -1,94 +0,0 @@
<?php
namespace AlibabaCloud\Credentials\Providers;
use AlibabaCloud\Credentials\Request\Request;
use AlibabaCloud\Credentials\StsCredential;
use Exception;
use GuzzleHttp\Exception\GuzzleException;
use AlibabaCloud\Tea\Response;
use InvalidArgumentException;
use Psr\Http\Message\ResponseInterface;
use RuntimeException;
/**
* Class EcsRamRoleProvider
*
* @package AlibabaCloud\Credentials\Providers
*/
class EcsRamRoleProvider extends Provider
{
/**
* Expiration time slot for temporary security credentials.
*
* @var int
*/
protected $expirationSlot = 10;
/**
* @var string
*/
private $uri = 'http://100.100.100.200/latest/meta-data/ram/security-credentials/';
/**
* Get credential.
*
* @return StsCredential
* @throws Exception
* @throws GuzzleException
*/
public function get()
{
$result = $this->getCredentialsInCache();
if ($result === null) {
$result = $this->request();
if (!isset($result['AccessKeyId'], $result['AccessKeySecret'], $result['SecurityToken'])) {
throw new RuntimeException($this->error);
}
$this->cache($result->toArray());
}
return new StsCredential(
$result['AccessKeyId'],
$result['AccessKeySecret'],
strtotime($result['Expiration']),
$result['SecurityToken']
);
}
/**
* Get credentials by request.
*
* @return ResponseInterface
* @throws Exception
* @throws GuzzleException
*/
public function request()
{
$credential = $this->credential;
$url = $this->uri . $credential->getRoleName();
$options = [
'http_errors' => false,
'timeout' => 1,
'connect_timeout' => 1,
];
$result = Request::createClient()->request('GET', $url, $options);
if ($result->getStatusCode() === 404) {
$message = 'The role was not found in the instance';
throw new InvalidArgumentException($message);
}
if ($result->getStatusCode() !== 200) {
throw new RuntimeException('Error retrieving credentials from result: ' . $result->toJson());
}
return $result;
}
}

View File

@@ -0,0 +1,65 @@
<?php
namespace AlibabaCloud\Credentials\Providers;
use AlibabaCloud\Credentials\Utils\Helper;
use InvalidArgumentException;
/**
* @internal This class is intended for internal use within the package.
* Class EnvironmentVariableCredentialsProvider
*
* @package AlibabaCloud\Credentials\Providers
*/
class EnvironmentVariableCredentialsProvider implements CredentialsProvider
{
/**
* EnvironmentVariableCredentialsProvider constructor.
*/
public function __construct() {}
/**
* Get credential.
*
* @return Credentials
* @throws InvalidArgumentException
*/
public function getCredentials()
{
if (Helper::envNotEmpty('ALIBABA_CLOUD_ACCESS_KEY_ID')) {
$accessKeyId = Helper::env('ALIBABA_CLOUD_ACCESS_KEY_ID');
} else {
throw new InvalidArgumentException('Access key ID must be specified via environment variable (ALIBABA_CLOUD_ACCESS_KEY_ID)');
}
if (Helper::envNotEmpty('ALIBABA_CLOUD_ACCESS_KEY_SECRET')) {
$accessKeySecret = Helper::env('ALIBABA_CLOUD_ACCESS_KEY_SECRET');
} else {
throw new InvalidArgumentException('Access key Secret must be specified via environment variable (ALIBABA_CLOUD_ACCESS_KEY_SECRET)');
}
if (Helper::envNotEmpty('ALIBABA_CLOUD_SECURITY_TOKEN')) {
$securityToken = Helper::env('ALIBABA_CLOUD_SECURITY_TOKEN');
return new Credentials([
'accessKeyId' => $accessKeyId,
'accessKeySecret' => $accessKeySecret,
'securityToken' => $securityToken,
'providerName' => $this->getProviderName(),
]);
}
return new Credentials([
'accessKeyId' => $accessKeyId,
'accessKeySecret' => $accessKeySecret,
'providerName' => $this->getProviderName(),
]);
}
/**
* @inheritDoc
*/
public function getProviderName()
{
return "env";
}
}

View File

@@ -0,0 +1,268 @@
<?php
namespace AlibabaCloud\Credentials\Providers;
use AlibabaCloud\Credentials\Utils\Helper;
use AlibabaCloud\Credentials\Utils\Filter;
use AlibabaCloud\Credentials\Request\Request;
use GuzzleHttp\Psr7\Uri;
use GuzzleHttp\Exception\GuzzleException;
use InvalidArgumentException;
use RuntimeException;
use Exception;
use AlibabaCloud\Credentials\Credential\RefreshResult;
/**
* @internal This class is intended for internal use within the package.
* Class OIDCRoleArnCredentialsProvider
*
* @package AlibabaCloud\Credentials\Providers
*/
class OIDCRoleArnCredentialsProvider extends SessionCredentialsProvider
{
/**
* @var string
*/
private $roleArn;
/**
* @var string
*/
private $oidcProviderArn;
/**
* @var string
*/
private $oidcTokenFilePath;
/**
* @var string
*/
private $roleSessionName;
/**
* @description role session expiration
* @example 3600
* @var int
*/
private $durationSeconds = 3600;
/**
* @var string
*/
private $policy;
/**
* @var string
*/
private $stsEndpoint;
/**
* @var int
*/
private $connectTimeout = 5;
/**
* @var int
*/
private $readTimeout = 5;
/**
* OIDCRoleArnCredentialsProvider constructor.
*
* @param array $params
* @param array $options
*/
public function __construct(array $params = [], array $options = [])
{
$this->filterOptions($options);
$this->filterRoleArn($params);
$this->filterOIDCProviderArn($params);
$this->filterOIDCTokenFilePath($params);
$this->filterRoleSessionName($params);
$this->filterDurationSeconds($params);
$this->filterPolicy($params);
$this->filterSTSEndpoint($params);
}
private function filterRoleArn(array $params)
{
if (Helper::envNotEmpty('ALIBABA_CLOUD_ROLE_ARN')) {
$this->roleArn = Helper::env('ALIBABA_CLOUD_ROLE_ARN');
}
if (isset($params['roleArn'])) {
$this->roleArn = $params['roleArn'];
}
Filter::roleArn($this->roleArn);
}
private function filterOIDCProviderArn(array $params)
{
if (Helper::envNotEmpty('ALIBABA_CLOUD_OIDC_PROVIDER_ARN')) {
$this->oidcProviderArn = Helper::env('ALIBABA_CLOUD_OIDC_PROVIDER_ARN');
}
if (isset($params['oidcProviderArn'])) {
$this->oidcProviderArn = $params['oidcProviderArn'];
}
Filter::oidcProviderArn($this->oidcProviderArn);
}
private function filterOIDCTokenFilePath(array $params)
{
if (Helper::envNotEmpty('ALIBABA_CLOUD_OIDC_TOKEN_FILE')) {
$this->oidcTokenFilePath = Helper::env('ALIBABA_CLOUD_OIDC_TOKEN_FILE');
}
if (isset($params['oidcTokenFilePath'])) {
$this->oidcTokenFilePath = $params['oidcTokenFilePath'];
}
Filter::oidcTokenFilePath($this->oidcTokenFilePath);
}
private function filterRoleSessionName(array $params)
{
if (Helper::envNotEmpty('ALIBABA_CLOUD_ROLE_SESSION_NAME')) {
$this->roleSessionName = Helper::env('ALIBABA_CLOUD_ROLE_SESSION_NAME');
}
if (isset($params['roleSessionName'])) {
$this->roleSessionName = $params['roleSessionName'];
}
if (is_null($this->roleSessionName) || $this->roleSessionName === '') {
$this->roleSessionName = 'phpSdkRoleSessionName';
}
}
private function filterDurationSeconds(array $params)
{
if (isset($params['durationSeconds'])) {
if (is_int($params['durationSeconds'])) {
$this->durationSeconds = $params['durationSeconds'];
}
}
if ($this->durationSeconds < 900) {
throw new InvalidArgumentException('Role session expiration should be in the range of 900s - max session duration');
}
}
private function filterPolicy(array $params)
{
if (isset($params['policy'])) {
if (is_string($params['policy'])) {
$this->policy = $params['policy'];
}
if (is_array($params['policy'])) {
$this->policy = json_encode($params['policy']);
}
}
}
private function filterSTSEndpoint(array $params)
{
$prefix = 'sts';
if (Helper::envNotEmpty('ALIBABA_CLOUD_VPC_ENDPOINT_ENABLED') || (isset($params['enableVpc']) && $params['enableVpc'] === true)) {
$prefix = 'sts-vpc';
}
if (Helper::envNotEmpty('ALIBABA_CLOUD_STS_REGION')) {
$this->stsEndpoint = $prefix . '.' . Helper::env('ALIBABA_CLOUD_STS_REGION') . '.aliyuncs.com';
}
if (isset($params['stsRegionId'])) {
$this->stsEndpoint = $prefix . '.' . $params['stsRegionId'] . '.aliyuncs.com';
}
if (isset($params['stsEndpoint'])) {
$this->stsEndpoint = $params['stsEndpoint'];
}
if (is_null($this->stsEndpoint) || $this->stsEndpoint === '') {
$this->stsEndpoint = 'sts.aliyuncs.com';
}
}
private function filterOptions(array $options)
{
if (isset($options['connectTimeout'])) {
$this->connectTimeout = $options['connectTimeout'];
}
if (isset($options['readTimeout'])) {
$this->readTimeout = $options['readTimeout'];
}
Filter::timeout($this->connectTimeout, $this->readTimeout);
}
/**
* Get credentials by request.
*
* @return RefreshResult
* @throws RuntimeException
* @throws GuzzleException
*/
public function refreshCredentials()
{
$options = Request::commonOptions();
$options['read_timeout'] = $this->readTimeout;
$options['connect_timeout'] = $this->connectTimeout;
$options['query']['Action'] = 'AssumeRoleWithOIDC';
$options['query']['Version'] = '2015-04-01';
$options['query']['Format'] = 'JSON';
$options['query']['Timestamp'] = gmdate('Y-m-d\TH:i:s\Z');
$options['query']['RoleArn'] = $this->roleArn;
$options['query']['OIDCProviderArn'] = $this->oidcProviderArn;
try {
$oidcToken = file_get_contents($this->oidcTokenFilePath);
$options['query']['OIDCToken'] = $oidcToken;
} catch (Exception $exception) {
throw new InvalidArgumentException($exception->getMessage());
}
$options['query']['RoleSessionName'] = $this->roleSessionName;
$options['query']['DurationSeconds'] = (string) $this->durationSeconds;
if (!is_null($this->policy)) {
$options['query']['Policy'] = $this->policy;
}
$url = (new Uri())->withScheme('https')->withHost($this->stsEndpoint);
$result = Request::createClient()->request('POST', $url, $options);
if ($result->getStatusCode() !== 200) {
throw new RuntimeException('Error refreshing credentials from OIDC, statusCode: ' . $result->getStatusCode() . ', result: ' . (string) $result);
}
$json = $result->toArray();
$credentials = $json['Credentials'];
if (!isset($credentials['AccessKeyId']) || !isset($credentials['AccessKeySecret']) || !isset($credentials['SecurityToken'])) {
throw new RuntimeException('Error retrieving credentials from OIDC result:' . $result->toJson());
}
return new RefreshResult(new Credentials([
'accessKeyId' => $credentials['AccessKeyId'],
'accessKeySecret' => $credentials['AccessKeySecret'],
'securityToken' => $credentials['SecurityToken'],
'expiration' => \strtotime($credentials['Expiration']),
'providerName' => $this->getProviderName(),
]), $this->getStaleTime(strtotime($credentials['Expiration'])));
}
public function key()
{
return 'oidc_role_arn#roleArn#' . $this->roleArn . '#oidcProviderArn#' . $this->oidcProviderArn . '#roleSessionName#' . $this->roleSessionName;
}
public function getProviderName()
{
return 'oidc_role_arn';
}
}

View File

@@ -0,0 +1,188 @@
<?php
namespace AlibabaCloud\Credentials\Providers;
use AlibabaCloud\Credentials\Utils\Helper;
use RuntimeException;
/**
* @internal This class is intended for internal use within the package.
* Class ProfileCredentialsProvider
*
* @package AlibabaCloud\Credentials\Providers
*/
class ProfileCredentialsProvider implements CredentialsProvider
{
/**
* @var string
*/
private $profileName;
/**
* @var string
*/
private $profileFile;
/**
* @var CredentialsProvider
*/
private $credentialsProvider;
/**
* ProfileCredentialsProvider constructor.
*
* @param array $params
*/
public function __construct(array $params = [])
{
$this->filterProfileName($params);
$this->filterProfileFile();
}
private function filterProfileName(array $params)
{
if (Helper::envNotEmpty('ALIBABA_CLOUD_PROFILE')) {
$this->profileName = Helper::env('ALIBABA_CLOUD_PROFILE');
}
if (isset($params['profileName'])) {
$this->profileName = $params['profileName'];
}
if (is_null($this->profileName) || $this->profileName === '') {
$this->profileName = 'default';
}
}
private function filterProfileFile()
{
$this->profileFile = Helper::envNotEmpty('ALIBABA_CLOUD_CREDENTIALS_FILE');
if (!$this->profileFile) {
$this->profileFile = self::getDefaultFile();
}
}
/**
* @return bool
*/
private function shouldReloadCredentialsProvider()
{
if (is_null($this->credentialsProvider)) {
return true;
}
return false;
}
/**
* @return CredentialsProvider
*/
private function reloadCredentialsProvider($profileFile, $profileName)
{
if (!Helper::inOpenBasedir($profileFile)) {
throw new RuntimeException('Unable to open credentials file: ' . $profileFile);
}
if (!\is_readable($profileFile) || !\is_file($profileFile)) {
throw new RuntimeException('Credentials file is not readable: ' . $profileFile);
}
$fileArray = \parse_ini_file($profileFile, true);
if (\is_array($fileArray) && !empty($fileArray)) {
$credentialsConfigures = [];
foreach (\array_change_key_case($fileArray) as $name => $configures) {
if ($name === $profileName) {
$credentialsConfigures = $configures;
break;
}
}
if (\is_array($credentialsConfigures) && !empty($credentialsConfigures)) {
switch (Helper::unsetReturnNull($credentialsConfigures, 'type')) {
case 'access_key':
return new StaticAKCredentialsProvider([
'accessKeyId' => Helper::unsetReturnNull($credentialsConfigures, 'access_key_id'),
'accessKeySecret' => Helper::unsetReturnNull($credentialsConfigures, 'access_key_secret'),
]);
case 'ram_role_arn':
$innerProvider = new StaticAKCredentialsProvider([
'accessKeyId' => Helper::unsetReturnNull($credentialsConfigures, 'access_key_id'),
'accessKeySecret' => Helper::unsetReturnNull($credentialsConfigures, 'access_key_secret'),
]);
return new RamRoleArnCredentialsProvider([
'credentialsProvider' => $innerProvider,
'roleArn' => Helper::unsetReturnNull($credentialsConfigures, 'role_arn'),
'roleSessionName' => Helper::unsetReturnNull($credentialsConfigures, 'role_session_name'),
'policy' => Helper::unsetReturnNull($credentialsConfigures, 'policy'),
]);
case 'ecs_ram_role':
return new EcsRamRoleCredentialsProvider([
'roleName' => Helper::unsetReturnNull($credentialsConfigures, 'role_name'),
]);
case 'oidc_role_arn':
return new OIDCRoleArnCredentialsProvider([
'roleArn' => Helper::unsetReturnNull($credentialsConfigures, 'role_arn'),
'oidcProviderArn' => Helper::unsetReturnNull($credentialsConfigures, 'oidc_provider_arn'),
'oidcTokenFilePath' => Helper::unsetReturnNull($credentialsConfigures, 'oidc_token_file_path'),
'roleSessionName' => Helper::unsetReturnNull($credentialsConfigures, 'role_session_name'),
'policy' => Helper::unsetReturnNull($credentialsConfigures, 'policy'),
]);
case 'rsa_key_pair':
return new RsaKeyPairCredentialsProvider([
'publicKeyId' => Helper::unsetReturnNull($credentialsConfigures, 'public_key_id'),
'privateKeyFile' => Helper::unsetReturnNull($credentialsConfigures, 'private_key_file'),
]);
default:
throw new RuntimeException('Unsupported credential type from credentials file: ' . Helper::unsetReturnNull($credentialsConfigures, 'type'));
}
}
}
throw new RuntimeException('Failed to get credential from credentials file: ' . $profileFile);
}
/**
* Get credential.
*
* @return Credentials
* @throws RuntimeException
*/
public function getCredentials()
{
if ($this->shouldReloadCredentialsProvider()) {
$this->credentialsProvider = $this->reloadCredentialsProvider($this->profileFile, $this->profileName);
}
$credentials = $this->credentialsProvider->getCredentials();
return new Credentials([
'accessKeyId' => $credentials->getAccessKeyId(),
'accessKeySecret' => $credentials->getAccessKeySecret(),
'securityToken' => $credentials->getSecurityToken(),
'providerName' => $this->getProviderName() . '/' . $this->credentialsProvider->getProviderName(),
]);
}
/**
* Get the default credential file.
*
* @return string
*/
private function getDefaultFile()
{
return Helper::getHomeDirectory() .
DIRECTORY_SEPARATOR .
'.alibabacloud' .
DIRECTORY_SEPARATOR .
'credentials';
}
/**
* @return string
*/
public function getProviderName()
{
return 'profile';
}
}

View File

@@ -1,82 +0,0 @@
<?php
namespace AlibabaCloud\Credentials\Providers;
use AlibabaCloud\Credentials\CredentialsInterface;
use AlibabaCloud\Credentials\EcsRamRoleCredential;
use AlibabaCloud\Credentials\RamRoleArnCredential;
use AlibabaCloud\Credentials\RsaKeyPairCredential;
abstract class Provider
{
/**
* For TSC Duration Seconds
*/
const DURATION_SECONDS = 3600;
/**
* @var array
*/
protected static $credentialsCache = [];
/**
* Expiration time slot for temporary security credentials.
*
* @var int
*/
protected $expirationSlot = 180;
/**
* @var RamRoleArnCredential|RsaKeyPairCredential|EcsRamRoleCredential
*/
protected $credential;
/**
* @var string
*/
protected $error = 'Result contains no credentials';
/**
* @var array
*/
protected $config = [];
/**
* CredentialTrait constructor.
*
* @param CredentialsInterface $credential
* @param array $config
*/
public function __construct(CredentialsInterface $credential, $config = [])
{
$this->credential = $credential;
$this->config = $config;
}
/**
* Get the credentials from the cache in the validity period.
*
* @return array|null
*/
public function getCredentialsInCache()
{
if (isset(self::$credentialsCache[(string)$this->credential])) {
$result = self::$credentialsCache[(string)$this->credential];
if (\strtotime($result['Expiration']) - \time() >= $this->expirationSlot) {
return $result;
}
}
return null;
}
/**
* Cache credentials.
*
* @param array $credential
*/
protected function cache(array $credential)
{
self::$credentialsCache[(string)$this->credential] = $credential;
}
}

View File

@@ -0,0 +1,321 @@
<?php
namespace AlibabaCloud\Credentials\Providers;
use AlibabaCloud\Credentials\Utils\Helper;
use AlibabaCloud\Credentials\Utils\Filter;
use AlibabaCloud\Credentials\Request\Request;
use GuzzleHttp\Psr7\Uri;
use GuzzleHttp\Exception\GuzzleException;
use InvalidArgumentException;
use RuntimeException;
use AlibabaCloud\Credentials\Credential\RefreshResult;
/**
* @internal This class is intended for internal use within the package.
* Class RamRoleArnCredentialsProvider
*
* @package AlibabaCloud\Credentials\Providers
*/
class RamRoleArnCredentialsProvider extends SessionCredentialsProvider
{
/**
* @var CredentialsProvider
*/
private $credentialsProvider;
/**
* @var string
*/
private $roleArn;
/**
* @var string
*/
private $roleSessionName;
/**
* @description role session expiration
* @example 3600
* @var int
*/
private $durationSeconds = 3600;
/**
* @var string
*/
private $externalId;
/**
* @var string
*/
private $policy;
/**
* @var string
*/
private $stsEndpoint;
/**
* @var int
*/
private $connectTimeout = 5;
/**
* @var int
*/
private $readTimeout = 5;
/**
* RamRoleArnCredentialsProvider constructor.
*
* @param array $params
* @param array $options
*/
public function __construct(array $params = [], array $options = [])
{
$this->filterOptions($options);
$this->filterCredentials($params);
$this->filterRoleArn($params);
$this->filterRoleSessionName($params);
$this->filterDurationSeconds($params);
$this->filterPolicy($params);
$this->filterExternalId($params);
$this->filterSTSEndpoint($params);
}
private function filterRoleArn(array $params)
{
if (Helper::envNotEmpty('ALIBABA_CLOUD_ROLE_ARN')) {
$this->roleArn = Helper::env('ALIBABA_CLOUD_ROLE_ARN');
}
if (isset($params['roleArn'])) {
$this->roleArn = $params['roleArn'];
}
Filter::roleArn($this->roleArn);
}
private function filterRoleSessionName(array $params)
{
if (Helper::envNotEmpty('ALIBABA_CLOUD_ROLE_SESSION_NAME')) {
$this->roleSessionName = Helper::env('ALIBABA_CLOUD_ROLE_SESSION_NAME');
}
if (isset($params['roleSessionName'])) {
$this->roleSessionName = $params['roleSessionName'];
}
if (is_null($this->roleSessionName) || $this->roleSessionName === '') {
$this->roleSessionName = 'phpSdkRoleSessionName';
}
}
private function filterDurationSeconds(array $params)
{
if (isset($params['durationSeconds'])) {
if (is_int($params['durationSeconds'])) {
$this->durationSeconds = $params['durationSeconds'];
}
}
if ($this->durationSeconds < 900) {
throw new InvalidArgumentException('Role session expiration should be in the range of 900s - max session duration');
}
}
private function filterPolicy(array $params)
{
if (isset($params['policy'])) {
if (is_string($params['policy'])) {
$this->policy = $params['policy'];
}
if (is_array($params['policy'])) {
$this->policy = json_encode($params['policy']);
}
}
}
private function filterExternalId(array $params)
{
if (isset($params['externalId'])) {
if (is_string($params['externalId'])) {
$this->externalId = $params['externalId'];
}
}
}
private function filterSTSEndpoint(array $params)
{
$prefix = 'sts';
if (Helper::envNotEmpty('ALIBABA_CLOUD_VPC_ENDPOINT_ENABLED') || (isset($params['enableVpc']) && $params['enableVpc'] === true)) {
$prefix = 'sts-vpc';
}
if (Helper::envNotEmpty('ALIBABA_CLOUD_STS_REGION')) {
$this->stsEndpoint = $prefix . '.' . Helper::env('ALIBABA_CLOUD_STS_REGION') . '.aliyuncs.com';
}
if (isset($params['stsRegionId'])) {
$this->stsEndpoint = $prefix . '.' . $params['stsRegionId'] . '.aliyuncs.com';
}
if (isset($params['stsEndpoint'])) {
$this->stsEndpoint = $params['stsEndpoint'];
}
if (is_null($this->stsEndpoint) || $this->stsEndpoint === '') {
$this->stsEndpoint = 'sts.aliyuncs.com';
}
}
private function filterCredentials(array $params)
{
if (isset($params['credentialsProvider'])) {
if (!($params['credentialsProvider'] instanceof CredentialsProvider)) {
throw new InvalidArgumentException('Invalid credentialsProvider option for ram_role_arn');
}
$this->credentialsProvider = $params['credentialsProvider'];
} else if (isset($params['accessKeyId']) && isset($params['accessKeySecret']) && isset($params['securityToken'])) {
Filter::accessKey($params['accessKeyId'], $params['accessKeySecret']);
Filter::securityToken($params['securityToken']);
$this->credentialsProvider = new StaticSTSCredentialsProvider($params);
} else if (isset($params['accessKeyId']) && isset($params['accessKeySecret'])) {
Filter::accessKey($params['accessKeyId'], $params['accessKeySecret']);
$this->credentialsProvider = new StaticAKCredentialsProvider($params);
} else {
throw new InvalidArgumentException('Missing required credentials option for ram_role_arn');
}
}
private function filterOptions(array $options)
{
if (isset($options['connectTimeout'])) {
$this->connectTimeout = $options['connectTimeout'];
}
if (isset($options['readTimeout'])) {
$this->readTimeout = $options['readTimeout'];
}
Filter::timeout($this->connectTimeout, $this->readTimeout);
}
/**
* Get credentials by request.
*
* @return RefreshResult
* @throws RuntimeException
* @throws GuzzleException
*/
public function refreshCredentials()
{
$options = Request::commonOptions();
$options['read_timeout'] = $this->readTimeout;
$options['connect_timeout'] = $this->connectTimeout;
$options['query']['Action'] = 'AssumeRole';
$options['query']['Version'] = '2015-04-01';
$options['query']['Format'] = 'JSON';
$options['query']['Timestamp'] = gmdate('Y-m-d\TH:i:s\Z');
$options['query']['SignatureMethod'] = 'HMAC-SHA1';
$options['query']['SignatureVersion'] = '1.0';
$options['query']['SignatureNonce'] = Request::uuid(json_encode($options['query']));
$options['query']['RoleArn'] = $this->roleArn;
$options['query']['RoleSessionName'] = $this->roleSessionName;
$options['query']['DurationSeconds'] = (string) $this->durationSeconds;
if (!is_null($this->policy) && $this->policy !== '') {
$options['query']['Policy'] = $this->policy;
}
if (!is_null($this->externalId) && $this->externalId !== '') {
$options['query']['ExternalId'] = $this->externalId;
}
$sessionCredentials = $this->credentialsProvider->getCredentials();
$options['query']['AccessKeyId'] = $sessionCredentials->getAccessKeyId();
if (!is_null($sessionCredentials->getSecurityToken())) {
$options['query']['SecurityToken'] = $sessionCredentials->getSecurityToken();
}
$options['query']['Signature'] = Request::shaHmac1sign(
Request::signString('GET', $options['query']),
$sessionCredentials->getAccessKeySecret() . '&'
);
$url = (new Uri())->withScheme('https')->withHost($this->stsEndpoint);
$result = Request::createClient()->request('GET', $url, $options);
if ($result->getStatusCode() !== 200) {
throw new RuntimeException('Error refreshing credentials from RamRoleArn, statusCode: ' . $result->getStatusCode() . ', result: ' . (string) $result);
}
$json = $result->toArray();
$credentials = $json['Credentials'];
if (!isset($credentials['AccessKeyId']) || !isset($credentials['AccessKeySecret']) || !isset($credentials['SecurityToken'])) {
throw new RuntimeException('Error retrieving credentials from RamRoleArn result:' . $result->toJson());
}
return new RefreshResult(new Credentials([
'accessKeyId' => $credentials['AccessKeyId'],
'accessKeySecret' => $credentials['AccessKeySecret'],
'securityToken' => $credentials['SecurityToken'],
'expiration' => \strtotime($credentials['Expiration']),
'providerName' => $this->getProviderName(),
]), $this->getStaleTime(strtotime($credentials['Expiration'])));
}
public function key()
{
$credentials = $this->credentialsProvider->getCredentials();
return 'ram_role_arn#credential#' . $credentials->getAccessKeyId() . '#roleArn#' . $this->roleArn . '#roleSessionName#' . $this->roleSessionName;
}
public function getProviderName()
{
return 'ram_role_arn/' . $this->credentialsProvider->getProviderName();
}
/**
* @return string
*/
public function getRoleArn()
{
return $this->roleArn;
}
/**
* @return string
*/
public function getRoleSessionName()
{
return $this->roleSessionName;
}
/**
* @return string
*/
public function getPolicy()
{
return $this->policy;
}
/**
* @deprecated
* @return string
*/
public function getOriginalAccessKeyId()
{
return $this->credentialsProvider->getCredentials()->getAccessKeyId();
}
/**
* @deprecated
* @return string
*/
public function getOriginalAccessKeySecret()
{
return $this->credentialsProvider->getCredentials()->getAccessKeySecret();
}
}

View File

@@ -1,49 +0,0 @@
<?php
namespace AlibabaCloud\Credentials\Providers;
use AlibabaCloud\Credentials\Request\AssumeRole;
use AlibabaCloud\Credentials\StsCredential;
use Exception;
use GuzzleHttp\Exception\GuzzleException;
use RuntimeException;
class RamRoleArnProvider extends Provider
{
/**
* Get credential.
*
* @return StsCredential
* @throws Exception
* @throws GuzzleException
*/
public function get()
{
$credential = $this->getCredentialsInCache();
if (null === $credential) {
$result = (new AssumeRole($this->credential))->request();
if ($result->getStatusCode() !== 200) {
throw new RuntimeException(isset($result['Message']) ? $result['Message'] : (string)$result->getBody());
}
if (!isset($result['Credentials']['AccessKeyId'],
$result['Credentials']['AccessKeySecret'],
$result['Credentials']['SecurityToken'])) {
throw new RuntimeException($this->error);
}
$credential = $result['Credentials'];
$this->cache($credential);
}
return new StsCredential(
$credential['AccessKeyId'],
$credential['AccessKeySecret'],
strtotime($credential['Expiration']),
$credential['SecurityToken']
);
}
}

View File

@@ -0,0 +1,200 @@
<?php
namespace AlibabaCloud\Credentials\Providers;
use AlibabaCloud\Credentials\Utils\Helper;
use AlibabaCloud\Credentials\Utils\Filter;
use AlibabaCloud\Credentials\Request\Request;
use GuzzleHttp\Psr7\Uri;
use GuzzleHttp\Exception\GuzzleException;
use AlibabaCloud\Credentials\Credential\RefreshResult;
use InvalidArgumentException;
use RuntimeException;
use Exception;
/**
* @internal This class is intended for internal use within the package.
* Class RsaKeyPairCredentialsProvider
*
* @package AlibabaCloud\Credentials\Providers
*/
class RsaKeyPairCredentialsProvider extends SessionCredentialsProvider
{
/**
* @var string
*/
private $publicKeyId;
/**
* @var string
*/
private $privateKey;
/**
* @description role session expiration
* @example 3600
* @var int
*/
private $durationSeconds = 3600;
/**
* @var string
*/
private $stsEndpoint;
/**
* @var int
*/
private $connectTimeout = 5;
/**
* @var int
*/
private $readTimeout = 5;
/**
* RsaKeyPairCredentialsProvider constructor.
*
* @param array $params
* @param array $options
*/
public function __construct(array $params = [], array $options = [])
{
$this->filterOptions($options);
$this->filterDurationSeconds($params);
$this->filterSTSEndpoint($params);
$this->publicKeyId = isset($params['publicKeyId']) ? $params['publicKeyId'] : null;
$privateKeyFile = isset($params['privateKeyFile']) ? $params['privateKeyFile'] : null;
Filter::publicKeyId($this->publicKeyId);
Filter::privateKeyFile($privateKeyFile);
try {
$this->privateKey = file_get_contents($privateKeyFile);
} catch (Exception $exception) {
throw new InvalidArgumentException($exception->getMessage());
}
}
private function filterOptions(array $options)
{
if (isset($options['connectTimeout'])) {
$this->connectTimeout = $options['connectTimeout'];
}
if (isset($options['readTimeout'])) {
$this->readTimeout = $options['readTimeout'];
}
Filter::timeout($this->connectTimeout, $this->readTimeout);
}
private function filterDurationSeconds(array $params)
{
if (isset($params['durationSeconds'])) {
if (is_int($params['durationSeconds'])) {
$this->durationSeconds = $params['durationSeconds'];
}
}
if ($this->durationSeconds < 900) {
throw new InvalidArgumentException('Role session expiration should be in the range of 900s - max session duration');
}
}
private function filterSTSEndpoint(array $params)
{
if (isset($params['stsEndpoint'])) {
$this->stsEndpoint = $params['stsEndpoint'];
}
if (is_null($this->stsEndpoint) || $this->stsEndpoint === '') {
$this->stsEndpoint = 'sts.ap-northeast-1.aliyuncs.com';
}
}
/**
* Get credentials by request.
*
* @return RefreshResult
* @throws RuntimeException
* @throws GuzzleException
*/
public function refreshCredentials()
{
$options = Request::commonOptions();
$options['read_timeout'] = $this->readTimeout;
$options['connect_timeout'] = $this->connectTimeout;
$options['query']['Action'] = 'GenerateSessionAccessKey';
$options['query']['Version'] = '2015-04-01';
$options['query']['Format'] = 'JSON';
$options['query']['Timestamp'] = gmdate('Y-m-d\TH:i:s\Z');
$options['query']['SignatureMethod'] = 'SHA256withRSA';
$options['query']['SignatureType'] = 'PRIVATEKEY';
$options['query']['SignatureVersion'] = '1.0';
$options['query']['SignatureNonce'] = Request::uuid(json_encode($options['query']));
$options['query']['DurationSeconds'] = (string) $this->durationSeconds;
$options['query']['AccessKeyId'] = $this->publicKeyId;
$options['query']['Signature'] = Request::shaHmac256WithRsasign(
Request::signString('GET', $options['query']),
$this->privateKey
);
$url = (new Uri())->withScheme('https')->withHost($this->stsEndpoint);
$result = Request::createClient()->request('GET', $url, $options);
if ($result->getStatusCode() !== 200) {
throw new RuntimeException('Error refreshing credentials from RsaKeyPair, statusCode: ' . $result->getStatusCode() . ', result: ' . (string) $result);
}
$json = $result->toArray();
if (!isset($json['SessionAccessKey']['SessionAccessKeyId']) || !isset($json['SessionAccessKey']['SessionAccessKeySecret'])) {
throw new RuntimeException('Error retrieving credentials from RsaKeyPair result:' . $result->toJson());
}
$credentials = [];
$credentials['AccessKeyId'] = $json['SessionAccessKey']['SessionAccessKeyId'];
$credentials['AccessKeySecret'] = $json['SessionAccessKey']['SessionAccessKeySecret'];
$credentials['Expiration'] = $json['SessionAccessKey']['Expiration'];
$credentials['SecurityToken'] = null;
return new RefreshResult(new Credentials([
'accessKeyId' => $credentials['AccessKeyId'],
'accessKeySecret' => $credentials['AccessKeySecret'],
'securityToken' => $credentials['SecurityToken'],
'expiration' => \strtotime($credentials['Expiration']),
'providerName' => $this->getProviderName(),
]), $this->getStaleTime(strtotime($credentials['Expiration'])));
}
public function key()
{
return 'rsa_key_pair#publicKeyId#' . $this->publicKeyId;
}
public function getProviderName()
{
return 'rsa_key_pair';
}
/**
* @return string
*/
public function getPublicKeyId()
{
return $this->publicKeyId;
}
/**
* @return mixed
*/
public function getPrivateKey()
{
return $this->privateKey;
}
}

View File

@@ -1,53 +0,0 @@
<?php
namespace AlibabaCloud\Credentials\Providers;
use AlibabaCloud\Credentials\Request\GenerateSessionAccessKey;
use AlibabaCloud\Credentials\StsCredential;
use Exception;
use GuzzleHttp\Exception\GuzzleException;
use RuntimeException;
/**
* Class RsaKeyPairProvider
*
* @package AlibabaCloud\Credentials\Providers
*/
class RsaKeyPairProvider extends Provider
{
/**
* Get credential.
*
*
* @return StsCredential
* @throws Exception
* @throws GuzzleException
*/
public function get()
{
$credential = $this->getCredentialsInCache();
if ($credential === null) {
$result = (new GenerateSessionAccessKey($this->credential))->request();
if ($result->getStatusCode() !== 200) {
throw new RuntimeException(isset($result['Message']) ? $result['Message'] : (string)$result->getBody());
}
if (!isset($result['SessionAccessKey']['SessionAccessKeyId'],
$result['SessionAccessKey']['SessionAccessKeySecret'])) {
throw new RuntimeException($this->error);
}
$credential = $result['SessionAccessKey'];
$this->cache($credential);
}
return new StsCredential(
$credential['SessionAccessKeyId'],
$credential['SessionAccessKeySecret'],
strtotime($credential['Expiration'])
);
}
}

View File

@@ -0,0 +1,161 @@
<?php
namespace AlibabaCloud\Credentials\Providers;
use AlibabaCloud\Credentials\Credential\RefreshResult;
abstract class SessionCredentialsProvider implements CredentialsProvider
{
/**
* @var array
*/
protected static $credentialsCache = [];
/**
* Expiration time slot for temporary security credentials.
*
* @var int
*/
protected $expirationSlot = 180;
/**
* @var string
*/
protected $error = 'Result contains no credentials';
/**
* Get the credentials from the cache in the validity period.
*
* @return RefreshResult|null
*/
protected function getCredentialsInCache()
{
if (isset(self::$credentialsCache[$this->key()])) {
$result = self::$credentialsCache[$this->key()];
return $result;
}
return null;
}
/**
* Cache credentials.
*
* @param RefreshResult $credential
*/
protected function cache(RefreshResult $credential)
{
self::$credentialsCache[$this->key()] = $credential;
}
/**
* Get credential.
*
* @return Credentials
*/
public function getCredentials()
{
if ($this->cacheIsStale() || $this->shouldInitiateCachePrefetch()) {
$result = $this->refreshCache();
$this->cache($result);
}
$result = $this->getCredentialsInCache();
return $result->credentials();
}
/**
* @return RefreshResult
*/
protected function refreshCache()
{
try {
return $this->handleFetchedSuccess($this->refreshCredentials());
} catch (\Exception $e) {
return $this->handleFetchedFailure($e);
}
}
/**
* @return RefreshResult
* @throws \Exception
*/
protected function handleFetchedFailure(\Exception $e)
{
$currentCachedValue = $this->getCredentialsInCache();
if (is_null($currentCachedValue)) {
throw $e;
}
if (time() < $currentCachedValue->staleTime()) {
return $currentCachedValue;
}
throw $e;
}
/**
* @return RefreshResult
*/
protected function handleFetchedSuccess(RefreshResult $value)
{
$now = time();
// 过期时间大于15分钟,不用管
if ($now < $value->staleTime()) {
return $value;
}
// 不足或等于15分钟,但未过期,下次会再次刷新
if ($now < $value->staleTime() + 15 * 60) {
$value->staleTime = $now;
return $value;
}
// 已过期,看缓存,缓存若大于15分钟,返回缓存,若小于15分钟,则稍后重试
if (is_null($this->getCredentialsInCache())) {
throw new \Exception("The fetched credentials have expired and no cache is available.");
} else if ($now < $this->getCredentialsInCache()->staleTime()) {
return $this->getCredentialsInCache();
} else {
// 返回成功,延长有效期 1 分钟
$expectation = mt_rand(50, 70);
$value->staleTime = time() + $expectation;
return $value;
}
}
/**
* @return bool
*/
protected function cacheIsStale()
{
return is_null($this->getCredentialsInCache()) || time() >= $this->getCredentialsInCache()->staleTime();
}
/**
* @return bool
*/
protected function shouldInitiateCachePrefetch()
{
return is_null($this->getCredentialsInCache()) || time() >= $this->getCredentialsInCache()->prefetchTime();
}
/**
* @return int
*/
public function getStaleTime($expiration)
{
return $expiration <= 0 ?
time() + (60 * 60) :
$expiration - (15 * 60);
}
/**
* @return RefreshResult
*/
abstract function refreshCredentials();
/**
* Get the toString of the credentials provider as the key.
*
* @return string
*/
abstract function key();
}

View File

@@ -0,0 +1,78 @@
<?php
namespace AlibabaCloud\Credentials\Providers;
use AlibabaCloud\Credentials\Utils\Helper;
use AlibabaCloud\Credentials\Utils\Filter;
/**
* @internal This class is intended for internal use within the package.
* Class StaticAKCredentialsProvider
*
* @package AlibabaCloud\Credentials\Providers
*/
class StaticAKCredentialsProvider implements CredentialsProvider
{
/**
* @var string
*/
private $accessKeyId;
/**
* @var string
*/
private $accessKeySecret;
/**
* StaticAKCredentialsProvider constructor.
*
* @param array $params
*/
public function __construct(array $params = [])
{
$this->filterAK($params);
}
private function filterAK(array $params)
{
if (Helper::envNotEmpty('ALIBABA_CLOUD_ACCESS_KEY_ID')) {
$this->accessKeyId = Helper::env('ALIBABA_CLOUD_ACCESS_KEY_ID');
}
if (Helper::envNotEmpty('ALIBABA_CLOUD_ACCESS_KEY_SECRET')) {
$this->accessKeySecret = Helper::env('ALIBABA_CLOUD_ACCESS_KEY_SECRET');
}
if (isset($params['accessKeyId'])) {
$this->accessKeyId = $params['accessKeyId'];
}
if (isset($params['accessKeySecret'])) {
$this->accessKeySecret = $params['accessKeySecret'];
}
Filter::accessKey($this->accessKeyId, $this->accessKeySecret);
}
/**
* Get credential.
*
* @return Credentials
*/
public function getCredentials()
{
return new Credentials([
'accessKeyId' => $this->accessKeyId,
'accessKeySecret' => $this->accessKeySecret,
'providerName' => $this->getProviderName(),
]);
}
/**
* @inheritDoc
*/
public function getProviderName()
{
return "static_ak";
}
}

View File

@@ -0,0 +1,92 @@
<?php
namespace AlibabaCloud\Credentials\Providers;
use AlibabaCloud\Credentials\Utils\Helper;
use AlibabaCloud\Credentials\Utils\Filter;
/**
* @internal This class is intended for internal use within the package.
* Class StaticSTSCredentialsProvider
*
* @package AlibabaCloud\Credentials\Providers
*/
class StaticSTSCredentialsProvider implements CredentialsProvider
{
/**
* @var string
*/
private $accessKeyId;
/**
* @var string
*/
private $accessKeySecret;
/**
* @var string
*/
private $securityToken;
/**
* StaticSTSCredentialsProvider constructor.
*
* @param array $params
*/
public function __construct(array $params = [])
{
$this->filterSTS($params);
}
private function filterSTS(array $params)
{
if (Helper::envNotEmpty('ALIBABA_CLOUD_ACCESS_KEY_ID')) {
$this->accessKeyId = Helper::env('ALIBABA_CLOUD_ACCESS_KEY_ID');
}
if (Helper::envNotEmpty('ALIBABA_CLOUD_ACCESS_KEY_SECRET')) {
$this->accessKeySecret = Helper::env('ALIBABA_CLOUD_ACCESS_KEY_SECRET');
}
if (Helper::envNotEmpty('ALIBABA_CLOUD_SECURITY_TOKEN')) {
$this->securityToken = Helper::env('ALIBABA_CLOUD_SECURITY_TOKEN');
}
if (isset($params['accessKeyId'])) {
$this->accessKeyId = $params['accessKeyId'];
}
if (isset($params['accessKeySecret'])) {
$this->accessKeySecret = $params['accessKeySecret'];
}
if (isset($params['securityToken'])) {
$this->securityToken = $params['securityToken'];
}
Filter::accessKey($this->accessKeyId, $this->accessKeySecret);
Filter::securityToken($this->securityToken);
}
/**
* Get credential.
*
* @return Credentials
*/
public function getCredentials()
{
return new Credentials([
'accessKeyId' => $this->accessKeyId,
'accessKeySecret' => $this->accessKeySecret,
'securityToken' => $this->securityToken,
'providerName' => $this->getProviderName(),
]);
}
/**
* @inheritDoc
*/
public function getProviderName()
{
return "static_sts";
}
}

View File

@@ -0,0 +1,126 @@
<?php
namespace AlibabaCloud\Credentials\Providers;
use AlibabaCloud\Credentials\Utils\Helper;
use AlibabaCloud\Credentials\Utils\Filter;
use AlibabaCloud\Credentials\Request\Request;
use GuzzleHttp\Exception\GuzzleException;
use InvalidArgumentException;
use RuntimeException;
use AlibabaCloud\Credentials\Credential\RefreshResult;
/**
* @internal This class is intended for internal use within the package.
* Class URLCredentialsProvider
*
* @package AlibabaCloud\Credentials\Providers
*/
class URLCredentialsProvider extends SessionCredentialsProvider
{
/**
* @var string
*/
private $credentialsURI;
/**
* @var int
*/
private $connectTimeout = 5;
/**
* @var int
*/
private $readTimeout = 5;
/**
* URLCredentialsProvider constructor.
*
* @param array $params
* @param array $options
*/
public function __construct(array $params = [], array $options = [])
{
$this->filterOptions($options);
$this->filterCredentialsURI($params);
}
private function filterOptions(array $options)
{
if (isset($options['connectTimeout'])) {
$this->connectTimeout = $options['connectTimeout'];
}
if (isset($options['readTimeout'])) {
$this->readTimeout = $options['readTimeout'];
}
Filter::timeout($this->connectTimeout, $this->readTimeout);
}
private function filterCredentialsURI(array $params)
{
if (Helper::envNotEmpty('ALIBABA_CLOUD_CREDENTIALS_URI')) {
$this->credentialsURI = Helper::env('ALIBABA_CLOUD_CREDENTIALS_URI');
}
if (isset($params['credentialsURI'])) {
$this->credentialsURI = $params['credentialsURI'];
}
Filter::credentialsURI($this->credentialsURI);
}
/**
* Get credentials by request.
*
* @return RefreshResult
* @throws InvalidArgumentException
* @throws RuntimeException
* @throws GuzzleException
*/
public function refreshCredentials()
{
$options = Request::commonOptions();
$options['read_timeout'] = $this->readTimeout;
$options['connect_timeout'] = $this->connectTimeout;
$result = Request::createClient()->request('GET', $this->credentialsURI, $options);
if ($result->getStatusCode() !== 200) {
throw new RuntimeException('Error refreshing credentials from credentialsURI, statusCode: ' . $result->getStatusCode() . ', result: ' . (string) $result);
}
$credentials = $result->toArray();
if (!isset($credentials['AccessKeyId']) || !isset($credentials['AccessKeySecret']) || !isset($credentials['SecurityToken']) || !isset($credentials['Expiration'])) {
throw new RuntimeException('Error retrieving credentials from credentialsURI result:' . $result->toJson());
}
return new RefreshResult(new Credentials([
'accessKeyId' => $credentials['AccessKeyId'],
'accessKeySecret' => $credentials['AccessKeySecret'],
'securityToken' => $credentials['SecurityToken'],
'expiration' => \strtotime($credentials['Expiration']),
'providerName' => $this->getProviderName(),
]), $this->getStaleTime(strtotime($credentials['Expiration'])));
}
/**
* @return string
*/
public function key()
{
return 'credential_uri#' . $this->credentialsURI;
}
/**
* @return string
*/
public function getProviderName()
{
return 'credential_uri';
}
}

View File

@@ -2,13 +2,16 @@
namespace AlibabaCloud\Credentials;
use AlibabaCloud\Credentials\Providers\RamRoleArnProvider;
use AlibabaCloud\Credentials\Providers\RamRoleArnCredentialsProvider;
use AlibabaCloud\Credentials\Credential\CredentialModel;
use AlibabaCloud\Credentials\Signature\ShaHmac1Signature;
use AlibabaCloud\Credentials\Utils\Filter;
use Exception;
use GuzzleHttp\Exception\GuzzleException;
use InvalidArgumentException;
/**
* @deprecated
* Use the AssumeRole of the RAM account to complete the authentication.
*/
class RamRoleArnCredential implements CredentialsInterface
@@ -177,13 +180,20 @@ class RamRoleArnCredential implements CredentialsInterface
}
/**
* @return StsCredential
* @return AlibabaCloud\Credentials\Providers\Credentials
* @throws Exception
* @throws GuzzleException
*/
protected function getSessionCredential()
{
return (new RamRoleArnProvider($this))->get();
$params = [
'accessKeyId' => $this->accessKeyId,
'accessKeySecret' => $this->accessKeyId,
'roleArn' => $this->roleArn,
'roleSessionName' => $this->roleSessionName,
'policy' => $this->policy,
];
return (new RamRoleArnCredentialsProvider($params))->getCredentials();
}
/**
@@ -215,4 +225,18 @@ class RamRoleArnCredential implements CredentialsInterface
{
return $this->getSessionCredential()->getExpiration();
}
/**
* @inheritDoc
*/
public function getCredential()
{
$credentials = $this->getSessionCredential();
return new CredentialModel([
'accessKeyId' => $credentials->getAccessKeyId(),
'accessKeySecret' => $credentials->getAccessKeySecret(),
'securityToken' => $credentials->getSecurityToken(),
'type' => 'ram_role_arn',
]);
}
}

Some files were not shown because too many files have changed in this diff Show More