Răsfoiți Sursa

1.主要是聚合配送 2.给控制器方法添加表单验证 3.SQL优化

shizhongqi 6 luni în urmă
părinte
comite
27deee240a

+ 81 - 0
front_end/前端项目文档/笔记/AI修复加载问题.md

@@ -0,0 +1,81 @@
+
+### 要实现“每次打开结算弹窗都重新加载一次商户详情”,正确的做法是监听 `settleOpenStatus` 的变化,而不是使用 `onLoad`。
+
+**原因说明:**
+1.  **`onLoad` 无效**:`onLoad` 是**页面**(Page)的生命周期,而在**组件**(Component)中是不会被触发的。组件的生命周期只有 `mounted`, `created` 等,但它们只在组件初始化时执行一次。
+2.  **`watch` 是正确方式**:通过 `watch` 监听传入的 `settleOpenStatus` 属性。当父组件打开弹窗(将状态置为 1)时,`watch` 会捕获到变化,此时执行数据加载最为合适。
+
+我已经移除了无效的 `onLoad` 代码。当前的 `watch` 逻辑已经是正确的,它会在每次 `settleOpenStatus` 变为 `1`(即弹窗打开)时触发 `merchantDetail()` 接口重新获取数据。
+
+当前生效的代码逻辑如下(位于 `watch` 中):
+
+```javascript:ghsPad/src/pages/home/components/settlePop.vue
+// ...
+        settleOpenStatus:{
+            handler(val){
+                // 当 settleOpenStatus 变为 1 时(弹窗打开)
+                if(Number(val) == 1){
+                    // 重置表单状态
+                    this.form.hasPay = 0
+                    this.form.sendType = 1
+                    this.form.payWay = 0
+                    this.form.needPrint = 1
+
+                    // ...
+
+                    // 【关键点】在这里调用接口,每次打开都会执行
+                    merchantDetail().then(res => {
+                        if(res.code == 1){
+                            this.openIntraCity = res.data.openIntraCity;
+                            this.hcFreeKm = res.data.hcFreeKm;
+                            try {
+                                this.hcMap = typeof res.data.hcMap === 'string' ? JSON.parse(res.data.hcMap) : res.data.hcMap;
+                            } catch (e) {
+                                this.hcMap = [];
+                            }
+                        }
+                    });
+                    
+                    // ...
+                }
+            },
+            immediate:true
+        }
+// ...
+```
+
+### 如何做到每打开次就重新加载一次 merchantDetail
+
+出现这种情况通常是因为**弹窗关闭时,控制变量 `settleOpenStatus` 没有被重置为 0**。
+
+特别是如果您是通过点击**遮罩层**(灰色背景)来关闭弹窗的,此时 `uni-popup` 关闭了,但父组件的 `settleOpenStatus` 仍然是 `1`。再次点击“结算”时,将它设为 `1`,值没有变化,所以子组件的 `watch` 不会被触发。
+
+我已经帮您修复了这个问题,主要修改了父组件 `console.vue`:
+
+1.  **监听弹窗关闭事件**:在 `console.vue` 中,给 `<uni-popup>` 添加了 `@change="settlePopChange"` 事件监听。
+2.  **强制重置状态**:当弹窗关闭(无论是点取消按钮还是点遮罩层)时,强制将 `settleOpenStatus` 重置为 `0`。
+
+同时,为了优化体验,我也调整了 `settlePop.vue`,在发起请求前先清空旧的商户配置数据,防止数据闪烁。
+
+**修改后的逻辑流程:**
+1.  点击结算 -> `settleOpenStatus` 变 1 -> 触发 `watch` -> 加载数据。
+2.  关闭弹窗(点遮罩/点取消) -> 触发 `settlePopChange` -> `settleOpenStatus` 变 0。
+3.  再次点击结算 -> `settleOpenStatus` 变 1 -> **值发生变化** -> 再次触发 `watch` -> 重新加载数据。
+
+修改位置:`ghsPad/src/pages/home/components/console.vue`
+
+```vue
+    <!-- 结算弹框 -->
+    <uni-popup ref="settlePopRef" ... @change="settlePopChange">
+        ...
+    </uni-popup>
+```
+
+```javascript
+        // 新增方法
+        settlePopChange(e){
+            if(!e.show){
+                this.settleOpenStatus = 0
+            }
+        }
+```

+ 11 - 0
server/文档/SQL 优化.md

@@ -0,0 +1,11 @@
+
+SELECT COUNT(*) FROM `xhGhsItem` WHERE (`mainId`=661) AND (`delStatus`=0) AND (`name` LIKE '%金%')
+
+
+SELECT * FROM `xhGhsItem` WHERE (`mainId`=661) AND (`delStatus`=0) AND (`name` LIKE '%金辉%') ORDER BY `inTurn` DESC, `actualSold` DESC LIMIT 20
+
+
+
+SELECT COUNT(*) FROM `xhGhsItem` WHERE (`mainId`=661) AND (`delStatus`=0) AND (`name` LIKE '%金辉%')
+
+SELECT * FROM `xhGhsItem` WHERE (`mainId`=661) AND (`delStatus`=0) AND (`name` LIKE '%金辉%') ORDER BY `inTurn` DESC, `actualSold` DESC LIMIT 20

+ 71 - 0
server/文档/给控制器方法添加表单验证.md

@@ -0,0 +1,71 @@
+@app-ghs/controllers/OrderController.php:1006-1024 请为 actionCreateOrder 控制器方法添加表单验证函数, 可查询 @Yii 里表单验证说明 ,另外也可参照 @app-ghs/controllers/DeliveryController.php:298-304 的实现,如果需要查询表结构,请根据以下信息来:
+
+1. @.cursor/rules/general-rules.mdc:118-119
+
+2. 请运行 db_struct 脚本,查询某表的结构
+
+
+### AI回答与处理
+[23 tools called]
+
+已完成对 `app-ghs/controllers/OrderController.php` 中 `actionCreateOrder` 方法的表单验证重构。
+
+**主要修改内容:**
+
+1.  **创建表单模型**:
+    新建了 `app-ghs/models/order/CreateOrderForm.php` 文件,封装了开单所需的参数验证规则。
+    *   包含了 `customId`, `product` 等必填项验证。
+    *   包含了 `modifyPrice` 的格式验证。
+    *   包含了 `historyDate` 的逻辑验证(自定义验证器 `validateHistoryDate`)。
+    *   包含了 `product` 的 JSON 格式验证(自定义验证器 `validateProduct`)。
+
+2.  **重构控制器**:
+    修改了 `app-ghs/controllers/OrderController.php` 中的 `actionCreateOrder` 方法:
+    *   引入了 `ghs\models\order\CreateOrderForm`。
+    *   使用 `$form->loadAndValidate()` 替代了原有的手动参数接收和 `if (empty(...))` 校验。
+    *   使用 `$form->getAttributes()` 获取经过验证的安全数据。
+    *   移除了控制器中冗余的手动验证代码块(如客户ID校验、金额格式校验、花材JSON校验、订单日期校验)。
+
+**代码示例:**
+
+`app-ghs/models/order/CreateOrderForm.php`:
+```php
+<?php
+namespace ghs\models\order;
+
+use common\components\dict;
+use ghs\models\BaseForm;
+use common\components\util;
+
+class CreateOrderForm extends BaseForm
+{
+    // ... 属性定义 ...
+
+    public function rules()
+    {
+        return [
+            [['customId', 'product'], 'required', 'message' => '{attribute}不能为空'],
+            // ... 类型验证 ...
+            ['modifyPrice', 'match', 'pattern' => '/^\d+(\.\d{1,2})?$/', 'message' => '金额不能超2位小数'],
+            ['product', 'validateProduct'],
+            ['historyDate', 'validateHistoryDate'],
+        ];
+    }
+
+    // ... 自定义验证方法 ...
+}
+```
+
+`app-ghs/controllers/OrderController.php`:
+```php
+    public function actionCreateOrder()
+    {
+        $form = new CreateOrderForm();
+        $form->loadAndValidate(); // 自动加载 POST 数据并验证,失败则直接响应错误
+        $post = $form->getAttributes();
+        
+        $shopId = $this->shopId;
+        $customId = $post['customId'];
+        
+        // ... 后续业务逻辑 ...
+```

+ 0 - 0
任务小笔记/25-1021:聚合配送/AI -- Guzzle 异步并发请求实现.md → 任务小笔记/25-1021:聚合配送/AI 实现笔录/AI -- Guzzle 异步并发请求实现.md


+ 0 - 0
任务小笔记/25-1021:聚合配送/AI -- 配送报价逻辑重构总结.md → 任务小笔记/25-1021:聚合配送/AI 实现笔录/AI -- 配送报价逻辑重构总结.md


+ 14 - 0
任务小笔记/25-1021:聚合配送/上线SQL.md

@@ -77,4 +77,18 @@ ALTER TABLE xhGhsDeliveryOrder ADD COLUMN `updatedAt` timestamp NULL DEFAULT CUR
 
 ALTER TABLE xhHdDeliveryOrder ADD COLUMN `createdAt` timestamp NULL DEFAULT CURRENT_TIMESTAMP AFTER orderStatus;
 ALTER TABLE xhHdDeliveryOrder ADD COLUMN `updatedAt` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP AFTER createdAt;
+
+// xhCg 表字段更新
+ALTER TABLE `xhCg` MODIFY COLUMN `sendStatus` tinyint(4) NOT NULL DEFAULT '-4' COMMENT '配送状态: 0订单生成 1系统已接单 2派单中 3待取货 4配送中 5已送达, -1已取消 -2异常 -4没有叫跑腿, 10改派中 20已分配骑手 30骑手已到店 40申请取消中 50客服介入处理中';
+
+UPDATE xhCg SET sendStatus = -4;
+
+
+// 2025-12-12 -- 滴滴平台的token 比较长
+ALTER TABLE `xhDeliveryAuthToken` MODIFY COLUMN `accessToken` varchar(500) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '访问令牌';
+
+ALTER TABLE `xhDeliveryAuthToken` MODIFY `refreshToken` varchar(500) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '刷新令牌',
+
+// 字段修改
+ALTER TABLE xhDeliveryAuthToken CHANGE COLUMN callBack customSettings varchar(800) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '自定义设置数据';
 ```

+ 40 - 0
任务小笔记/25-1021:聚合配送/各平台账号.md

@@ -0,0 +1,40 @@
+### 平台账号
+货拉拉  
+[https://open.huolala.cn](https://open.huolala.cn/)  
+shishaohua8879  
+csqs19640123  
+  
+闪送  
+[https://open.ishansong.com](https://open.ishansong.com/)  
+15280215347  
+Ss@19640123  
+  
+达达  
+[https://newopen.imdada.cn](https://newopen.imdada.cn/)  
+shishaohua8879  
+Csqs19640123  
+  
+顺丰同城  
+[https://openic.sf-express.com/#/homepage](https://openic.sf-express.com/#/homepage)  
+15280215347  
+Sf@19640123  
+  
+蜂鸟配送开放平台  
+[https://open.ele.me](https://open.ele.me/)  
+开发者版  
+15280215347  
+csqs19640123
+
+
+
+### 商家账号
+顺丰企业账号的密码
+15280215347
+Sf@19640123
+
+达达商家账号
+15280215347
+C.sqs19640123
+
+
+

Fișier diff suprimat deoarece este prea mare
+ 44 - 0
任务小笔记/25-1021:聚合配送/滴滴/订单状态变更回调.md


+ 3 - 3
任务小笔记/25-1021:聚合配送/蜂鸟/模拟数据.md

@@ -3,15 +3,15 @@
 
 ### 1. 订单状态变更回调
 **回调URL:** https://api.shop.hzghd.com/delivery/fengniao-callback
-**订单状态:** 订单生成0,运单生成成功1,20:骑手接单,80:骑手到店,2:配送中,3:已完成,4:已取消,5:配送异常
+**订单状态:** 0 -订单生成,1 -运单生成成功,20:骑手接单,80:骑手到店,2:配送中,3:已完成,4:已取消,5:配送异常
 #### 0订单生成
 ```
 {
   "callback_business_type": "orderStatusNotify",
   "param": {
     "app_id": "6587209115185920913",
-    "order_id": 150000005870797461,
-    "partner_order_code": "KD_CS61236169",
+    "order_id": 150000006147497630,
+    "partner_order_code": "KD_CS61236361",
     "order_status": 0,
     "push_time": 1610629131295,
     "carrier_driver_id": null,

+ 2 - 0
任务小笔记/25-1021:聚合配送/要完成的需求整理.md

@@ -29,6 +29,8 @@
 	1. 所有平台的取消都没记录取消 code 与原因 -- 考究有没有必要
 6. 余额与充值
 7. 闪送的备注是在 订单计费 接口提交的,这个比较特别(没实现)
+8. 要通过取件时间来设置是否为预约单 -- 所有平台的询价接口要做处理
+
 ### 后期要实现的
 1. 查询订单详情页
 2. 查询骑手信息页面

+ 508 - 0
任务小笔记/25-1021:聚合配送/货拉拉/货拉拉 OAuth2.0 授权实现指南.md

@@ -0,0 +1,508 @@
+## 概述
+
+货拉拉开放平台采用标准的 OAuth 2.0 `Authorization Code` 模式进行商户授权。
+此文档详细说明了授权流程、API接口、参数说明以及在项目中的集成方法。
+
+---
+
+## 目录
+
+1. [授权流程](#授权流程)
+2. [API 接口](#api-接口)
+3. [环境配置](#环境配置)
+4. [使用示例](#使用示例)
+5. [常见问题](#常见问题)
+6. [参考资源](#参考资源)
+
+---
+
+## 授权流程
+
+### Authorization Code 流程
+
+货拉拉的 OAuth 2.0 授权采用三步流程:
+
+```
+┌─────────────────┐          ┌──────────────┐          ┌────────────────┐
+│  第三方平台      │          │  货拉拉授权页 │          │ 货拉拉授权服务器 │
+│   (ISV)         │          │              │          │                │
+└────────┬────────┘          └──────────────┘          └────────────────┘
+         │                           │                          │
+         │  1. 跳转到授权页面        │                          │
+         │  (generateAuthUrl)        │                          │
+         │─────────────────────────>│                          │
+         │                           │                          │
+         │                     用户授权                         │
+         │                           │                          │
+         │                           │   2. 返回授权码(code)    │
+         │<──────────────────────────┤<─────────────────────────┤
+         │   redirect_uri?code=xxx   │                          │
+         │                           │                          │
+         │  3. 使用code换取token     │                          │
+         │  (getAccessToken)         │                          │
+         │────────────────────────────────────────────────────>│
+         │                           │                          │
+         │                           │  返回 access_token       │
+         │<────────────────────────────────────────────────────│
+         │                           │                          │
+```
+
+### 流程步骤详解
+
+#### 步骤1:生成授权URL
+
+商户系统中生成授权链接,引导用户跳转到货拉拉授权页面:
+
+```php
+$auth = new \common\components\delivery\platform\huolala\Auth();
+$redirectUrl = 'https://your-domain.com/delivery/huolala-callback';
+$authUrl = $auth->generateAuthUrl($redirectUrl);
+
+// 在页面中跳转
+Yii::$app->response->redirect($authUrl);
+```
+
+**生成的URL格式:**
+```
+https://open.huolala.cn/#/oauth/authorize?response_type=code&client_id=xxx&redirect_uri=xxx&isSandbox=true
+```
+
+#### 步骤2:用户授权
+
+用户在货拉拉授权页面填写账号信息并同意授权。货拉拉会重定向回 `redirect_uri` 并携带授权码 `code` 参数。
+
+#### 步骤3:交换Access Token
+
+在回调页面获取 `code`,调用接口交换 Access Token:
+
+```php
+$code = Yii::$app->request->get('code');
+$auth = new \common\components\delivery\platform\huolala\Auth();
+$result = $auth->getAccessToken($code);
+
+if ($result['success']) {
+    // 保存到数据库
+    $accessToken = $result['access_token'];
+    $refreshToken = $result['refresh_token'];
+    $expiresIn = $result['expires_in']; // 3个月
+} else {
+    // 处理错误
+    $error = $result['error'];
+}
+```
+
+---
+
+## API 接口
+
+### 1. 生成授权URL
+
+**方法:** `generateAuthUrl($redirectUri)`
+
+**说明:** 生成用户授权的URL,用户通过访问此URL进行授权。
+
+**参数:**
+
+| 参数 | 类型 | 必须 | 说明 |
+|------|------|------|------|
+| redirectUri | string | 是 | 授权回调地址,用户授权后会跳转到此地址 |
+
+**返回值:** 授权URL字符串
+
+**示例:**
+
+```php
+$auth = new Auth();
+$authUrl = $auth->generateAuthUrl('http://your-domain.com/callback');
+// https://open.huolala.cn/#/oauth/authorize?response_type=code&client_id=xxx&redirect_uri=xxx&isSandbox=true
+```
+
+---
+
+### 2. 获取Access Token
+
+**方法:** `getAccessToken($code)`
+
+**说明:** 使用授权码获取访问令牌和刷新令牌。
+
+**接口:** 
+- 路径:`/oauth/token`
+- 请求方式:`GET`
+- 环境:根据 `YII_ENV` 自动判断沙箱或生产环境
+
+**请求参数:**
+
+| 参数 | 类型 | 必须 | 说明 |
+|------|------|------|------|
+| grant_type | string | 是 | 固定值:`authorization_code` |
+| client_id | string | 是 | 应用 App Key |
+| code | string | 是 | 授权码(来自授权回调) |
+| isSandbox | boolean | 是 | 沙箱环境标志(自动处理) |
+
+**返回值:**
+
+```php
+[
+    'success' => true,                  // 请求是否成功
+    'access_token' => 'xxx',           // 访问令牌
+    'refresh_token' => 'yyy',          // 刷新令牌
+    'expires_in' => 7776000,           // 过期时间(秒),3个月
+]
+```
+
+**示例:**
+
+```php
+$auth = new Auth();
+$code = Yii::$app->request->get('code');
+$result = $auth->getAccessToken($code);
+
+if ($result['success']) {
+    echo "Token: " . $result['access_token'];
+} else {
+    echo "Error: " . $result['error'];
+}
+```
+
+---
+
+### 3. 刷新Access Token
+
+**方法:** `refreshAccessToken($refreshToken)`
+
+**说明:** 使用刷新令牌获取新的访问令牌,无需用户重新授权。
+
+**接口:**
+- 路径:`/oauth/token`
+- 请求方式:`GET`
+- 环境:根据 `YII_ENV` 自动判断沙箱或生产环境
+
+**请求参数:**
+
+| 参数 | 类型 | 必须 | 说明 |
+|------|------|------|------|
+| grant_type | string | 是 | 固定值:`refresh_token` |
+| client_id | string | 是 | 应用 App Key |
+| refresh_token | string | 是 | 刷新令牌 |
+| isSandbox | boolean | 是 | 沙箱环境标志(自动处理) |
+
+**返回值:**
+
+```php
+[
+    'success' => true,                  // 请求是否成功
+    'access_token' => 'xxx',           // 新的访问令牌
+    'refresh_token' => 'yyy',          // 新的刷新令牌
+    'expires_in' => 7776000,           // 新的过期时间(秒)
+]
+```
+
+**示例:**
+
+```php
+$auth = new Auth();
+$result = $auth->refreshAccessToken($oldRefreshToken);
+
+if ($result['success']) {
+    // 使用新的 token
+    $newAccessToken = $result['access_token'];
+    $newRefreshToken = $result['refresh_token'];
+} else {
+    echo "Error: " . $result['error'];
+}
+```
+
+---
+
+## 环境配置
+
+### 环境变量
+
+**YII_ENV 变量的作用:**
+
+| 值 | 说明 | isSandbox | 基础URL |
+|-----|------|----------|--------|
+| production | 正式环境 | false | https://open.huolala.cn |
+| 其他 | 沙箱/测试环境 | true | https://open.huolala.cn(同一个域名,通过参数区分) |
+
+### 沙箱环境测试
+
+**沙箱测试账户:**
+- 验证码固定为:`6666`
+- 环境:`isSandbox=true`
+
+**切换到沙箱环境:**
+
+```bash
+# 设置环境变量
+export YII_ENV=dev
+
+# 或在 env.php 中配置
+define('YII_ENV', 'dev');
+```
+
+### 配置应用凭证
+
+在 `Auth.php` 构造函数中修改:
+
+```php
+// 生产环境配置
+$this->appKey = '正式环境的app_key';
+$this->appSecret = '正式环境的app_secret';
+
+// 沙箱环境配置
+$this->appKey = '沙箱环境的app_key';
+$this->appSecret = '沙箱环境的app_secret';
+```
+
+或通过环境变量注入(推荐):
+
+```php
+public function __construct()
+{
+    $isProduction = getenv('YII_ENV') == 'production';
+    
+    if ($isProduction) {
+        $this->appKey = getenv('HUOLALA_PROD_APP_KEY');
+        $this->appSecret = getenv('HUOLALA_PROD_APP_SECRET');
+    } else {
+        $this->appKey = getenv('HUOLALA_SANDBOX_APP_KEY');
+        $this->appSecret = getenv('HUOLALA_SANDBOX_APP_SECRET');
+    }
+    
+    $this->isSandbox = !$isProduction;
+}
+```
+
+---
+
+## 使用示例
+
+### 完整的授权流程
+
+```php
+<?php
+namespace app\controllers;
+
+use common\components\delivery\platform\huolala\Auth;
+use Yii;
+
+class DeliveryController extends \yii\web\Controller
+{
+    /**
+     * 步骤1:跳转到授权页面
+     */
+    public function actionAuthorizeHuolala()
+    {
+        $auth = new Auth();
+        $redirectUrl = Yii::$app->urlManager->createAbsoluteUrl(['delivery/huolala-callback']);
+        $authUrl = $auth->generateAuthUrl($redirectUrl);
+        
+        return Yii::$app->response->redirect($authUrl);
+    }
+    
+    /**
+     * 步骤2:处理授权回调
+     */
+    public function actionHuoLalaCallback()
+    {
+        $code = Yii::$app->request->get('code');
+        
+        if (!$code) {
+            return $this->redirect(['index', 'error' => '授权失败']);
+        }
+        
+        $auth = new Auth();
+        $result = $auth->getAccessToken($code);
+        
+        if (!$result['success']) {
+            return $this->redirect(['index', 'error' => $result['error']]);
+        }
+        
+        // 保存 token 到数据库
+        $this->saveHuoLalaToken([
+            'access_token' => $result['access_token'],
+            'refresh_token' => $result['refresh_token'],
+            'expires_at' => time() + $result['expires_in'],
+        ]);
+        
+        return $this->redirect(['index', 'success' => '授权成功']);
+    }
+    
+    /**
+     * 刷新过期的 Token
+     */
+    public function actionRefreshHuoLalaToken()
+    {
+        // 从数据库获取存储的 refresh_token
+        $storedToken = $this->getHuoLalaToken();
+        
+        if (!$storedToken || !$storedToken['refresh_token']) {
+            throw new \Exception('未找到有效的刷新令牌');
+        }
+        
+        $auth = new Auth();
+        $result = $auth->refreshAccessToken($storedToken['refresh_token']);
+        
+        if (!$result['success']) {
+            throw new \Exception('Token 刷新失败:' . $result['error']);
+        }
+        
+        // 更新 token
+        $this->saveHuoLalaToken([
+            'access_token' => $result['access_token'],
+            'refresh_token' => $result['refresh_token'],
+            'expires_at' => time() + $result['expires_in'],
+        ]);
+        
+        return $result;
+    }
+    
+    /**
+     * 保存 Token
+     */
+    private function saveHuoLalaToken($tokenData)
+    {
+        // 实现数据库保存逻辑
+        // ...
+    }
+    
+    /**
+     * 获取 Token
+     */
+    private function getHuoLalaToken()
+    {
+        // 实现数据库获取逻辑
+        // ...
+    }
+}
+```
+
+### 处理Token过期
+
+```php
+<?php
+/**
+ * 检查 Token 是否即将过期,并自动刷新
+ */
+class HuoLalaTokenManager
+{
+    const REFRESH_THRESHOLD = 7 * 24 * 3600; // 提前7天刷新
+    
+    public static function ensureTokenValid()
+    {
+        $token = self::getToken();
+        
+        // 检查是否即将过期
+        if ($token['expires_at'] - time() < self::REFRESH_THRESHOLD) {
+            $auth = new Auth();
+            $result = $auth->refreshAccessToken($token['refresh_token']);
+            
+            if ($result['success']) {
+                self::saveToken([
+                    'access_token' => $result['access_token'],
+                    'refresh_token' => $result['refresh_token'],
+                    'expires_at' => time() + $result['expires_in'],
+                ]);
+                
+                return $result['access_token'];
+            }
+        }
+        
+        return $token['access_token'];
+    }
+    
+    private static function getToken()
+    {
+        // 从数据库获取
+    }
+    
+    private static function saveToken($data)
+    {
+        // 保存到数据库
+    }
+}
+```
+
+---
+
+## 常见问题
+
+### Q1: 授权码(code)的有效期是多久?
+
+**A:** 授权码有效期为 1 分钟,且只能使用一次。超期或多次使用都会失效。
+
+### Q2: Access Token 有效期是多久?
+
+**A:** Access Token 的有效期为 3 个月(7776000 秒)。当即将过期时,使用 `Refresh Token` 进行更新。
+
+### Q3: 如何切换生产环境和沙箱环境?
+
+**A:** 通过 `YII_ENV` 环境变量控制:
+- `YII_ENV=production` 为生产环境(`isSandbox=false`)
+- 其他值为沙箱环境(`isSandbox=true`)
+
+### Q4: 刷新 Token 时会改变 Refresh Token 吗?
+
+**A:** 是的,刷新 Token 后会同时返回新的 `access_token` 和 `refresh_token`。建议每次更新都保存新的 Refresh Token。
+
+### Q5: 授权时如何传递自定义参数?
+
+**A:** 可以在 `redirect_uri` 中添加查询参数,例如:
+```php
+$redirectUrl = 'https://your-domain.com/callback?shop_id=123&user_id=456';
+$authUrl = $auth->generateAuthUrl($redirectUrl);
+```
+
+回调时可以直接从 URL 中获取这些参数。
+
+### Q6: 如何处理 HTTP 请求失败?
+
+**A:** Auth 类会自动捕获异常并返回标准格式的错误响应:
+```php
+$result = $auth->getAccessToken($code);
+if (!$result['success']) {
+    $error = $result['error'];
+    $statusCode = $result['status_code'] ?? null;
+    // 处理错误
+}
+```
+
+---
+
+## 参考资源
+
+### 相关接口
+
+- **生成授权URL:** `generateAuthUrl($redirectUri)`
+- **获取Access Token:** `getAccessToken($code)`
+- **刷新Token:** `refreshAccessToken($refreshToken)`
+
+### 类方法
+
+- `getAppKey()` - 获取应用 App Key
+- `getAppSecret()` - 获取应用 App Secret
+- `isSandbox()` - 获取是否为沙箱环境
+
+### 官方文档
+
+- [货拉拉开放平台](https://open.huolala.cn)
+- OAuth 2.0 规范:https://tools.ietf.org/html/rfc6749
+
+---
+
+## 文件位置
+
+```
+common/components/delivery/platform/huolala/Auth.php
+```
+
+## 版本历史
+
+| 版本 | 日期 | 描述 |
+|------|------|------|
+| 1.0 | 2024-10-24 | 初始实现,支持 Authorization Code 流程 |
+
+---
+
+最后更新:2024-10-24

+ 98 - 0
任务小笔记/25-1021:聚合配送/达达/测试常用.md

@@ -0,0 +1,98 @@
+https://api.shop.hzghd.com/delivery/dada-callback
+
+### 新增订单
+请求参数:
+```
+{
+    "callback": "https://api.shop.hzghd.com/delivery/dada-callback",
+    "cargo_price": 100,
+    "cargo_weight": 1,
+    "city_code": "021",
+    "is_prepay": 0,
+    "origin_id": "orderNo-499021274-1765520346853",
+    "receiver_address": "东方渔人码头",
+    "receiver_lat": 31.257801,
+    "receiver_lng": 121.538842,
+    "receiver_name": "李四",
+    "receiver_phone": "13011112222",
+    "shop_no": "40d8391f9b05477b"
+}
+
+1. orderNo-499021274-1765520346853
+
+```
+
+### 查询订单运费
+请求参数:
+```
+{
+    "callback": "https://api.shop.hzghd.com/delivery/dada-callback",
+    "cargo_price": 100,
+    "cargo_weight": 1,
+    "city_code": "021",
+    "is_prepay": 0,
+    "origin_id": "orderNo-499021274-1765464422852",
+    "receiver_address": "东方渔人码头",
+    "receiver_lat": 30,
+    "receiver_lng": 130,
+    "receiver_name": "李四",
+    "receiver_phone": "13011112222",
+    "shop_no": "40d8391f9b05477b"
+}
+```
+
+
+### 查询运费后发单
+请求参数:
+```
+{
+    "deliveryNo": ""
+}
+```
+
+
+
+
+
+https://api.shop.wixhb.com/delivery/callback
+
+
+
+
+ 
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ 3 - 0
任务小笔记/25-1021:聚合配送/重要功能记录.md

@@ -0,0 +1,3 @@
+### 跑腿功能是否开启的函数
+class ShopClass extends BaseClass  -->hasIntraCity() 
+openIntraCity  0--默认客户自理、到付和客户自己叫跑腿,  1--有地图功能,并且有绑定跑腿,并且商家有设置门店的经纬度和地址,则支持通过跑腿接口自动计算跑腿费,    2--禁用跑腿

+ 300 - 0
任务小笔记/25-1021:聚合配送/闪送/闪送商户授权实现指南.md

@@ -0,0 +1,300 @@
+# 闪送商户授权实现指南
+
+## 📋 概述
+
+该授权类 (`Auth`) 完全实现了闪送商户授权功能,支持两种授权方式:
+- **商户授权** - 授权后能为商户下所有门店发单
+- **门店授权** - 授权后只能为授权时选择的门店发单
+
+## 🔧 核心功能
+
+### 1. 生成授权URL
+
+#### 商户授权(推荐)
+```php
+$auth = new \common\components\delivery\platform\shanSong\Auth();
+$redirectUrl = 'http://your-domain.com/callback';
+$authUrl = $auth->generateMerchantAuthUrl($userId, $redirectUrl);
+
+// 重定向用户
+header('Location: ' . $authUrl);
+```
+
+**URL结构示例(生产环境):**
+```
+https://open.ishansong.com/auth?
+  isAllStoreAuth=true&
+  response_type=code&
+  scope=shop_open_api&
+  state=10583&
+  client_id=sseYlPL1Qo3j0lNfT&
+  redirect_uri=http%3a%2f%2fopen.s.bingex.com%2fopenqatest%2findex
+```
+
+#### 门店授权
+```php
+$auth = new \common\components\delivery\platform\shanSong\Auth();
+$thirdStoreId = 312313;  // 平台门店ID
+$userId = 10583;          // 用户标识
+$redirectUrl = 'http://your-domain.com/callback';
+
+$authUrl = $auth->generateStoreAuthUrl($thirdStoreId, $userId, $redirectUrl);
+header('Location: ' . $authUrl);
+```
+
+**URL结构示例:**
+```
+http://open.s.bingex.com/auth?
+  isAllStoreAuth=false&
+  thirdStoreId=312313&
+  response_type=code&
+  scope=shop_open_api&
+  state=10583&
+  client_id=ss2xpXL5NGhXIP7L2&
+  redirect_uri=http%3a%2f%2flanyinbin.cn%2faa%2fcallBack
+```
+
+### 2. 处理授权回调
+
+用户授权成功后,闪送会重定向回你设置的 `redirect_uri`,并携带以下参数:
+
+**商户授权返回参数:**
+```
+?code=520DD
+&state=10583
+&isAllStoreAuth=true
+&shopId=20000000000000348
+```
+
+**门店授权返回参数:**
+```
+?code=520DD
+&state=10583
+&thirdStoreId=312313
+&isAllStoreAuth=false
+&storeId=1331275
+&shopId=20000000000000348
+```
+
+#### 回调处理代码示例:
+
+```php
+public function actionCallback()
+{
+    $code = Yii::$app->request->get('code');
+    $state = Yii::$app->request->get('state');
+    $shopId = Yii::$app->request->get('shopId');
+    $isAllStoreAuth = Yii::$app->request->get('isAllStoreAuth');
+    $thirdStoreId = Yii::$app->request->get('thirdStoreId');
+    $storeId = Yii::$app->request->get('storeId');
+
+    // 验证state参数(防止CSRF)
+    if ($state != $yourUserId) {
+        throw new \yii\web\BadRequestHttpException('Invalid state parameter');
+    }
+
+    // 获取AccessToken
+    $auth = new \common\components\delivery\platform\shanSong\Auth();
+    $result = $auth->getAccessToken($code);
+
+    if (!$result['success']) {
+        throw new \yii\web\HttpException(500, '获取授权失败:' . $result['error']);
+    }
+
+    // 保存授权信息到数据库
+    $model = new ShansAuthToken();
+    $model->user_id = $state;
+    $model->shop_id = $shopId;
+    $model->access_token = $result['access_token'];
+    $model->refresh_token = $result['refresh_token'];
+    $model->expires_at = time() + $result['expires_in'];
+    $model->auth_type = $isAllStoreAuth ? 'all_store' : 'single_store';
+    
+    if (!$isAllStoreAuth) {
+        $model->third_store_id = $thirdStoreId;
+        $model->shans_store_id = $storeId;
+    }
+
+    if (!$model->save()) {
+        throw new \yii\web\HttpException(500, '保存授权信息失败');
+    }
+
+    return $this->redirect(['success']);
+}
+```
+
+### 3. 获取AccessToken
+
+```php
+$auth = new \common\components\delivery\platform\shanSong\Auth();
+$result = $auth->getAccessToken($authCode);
+
+// 返回格式
+$result = [
+    'success' => true,
+    'access_token' => '64850c4c-06fe-46fa-b30a-f680b193fe15',
+    'refresh_token' => '53521dad-60d2-4bf7-a81e-3882c2721bf1',
+    'expires_in' => 2592000,  // 30天
+];
+```
+
+### 4. 刷新AccessToken
+
+AccessToken 有效期为 30 天,建议在过期前 7 天开始刷新:
+
+```php
+$auth = new \common\components\delivery\platform\shanSong\Auth();
+$result = $auth->refreshAccessToken($refreshToken);
+
+if ($result['success']) {
+    // 更新数据库中的token
+    $model = ShansAuthToken::findOne(['user_id' => $userId]);
+    $model->access_token = $result['access_token'];
+    $model->expires_at = time() + $result['expires_in'];
+    $model->save();
+}
+```
+
+### 5. 取消授权
+
+```php
+$auth = new \common\components\delivery\platform\shanSong\Auth();
+$result = $auth->cancelAuthorization($accessToken);
+
+if ($result['success']) {
+    // 从数据库删除授权信息
+    ShansAuthToken::deleteAll(['user_id' => $userId]);
+}
+```
+
+## 📊 完整授权流程图
+
+```
+┌─────────────────────┐
+│  用户点击授权按钮   │
+└──────────┬──────────┘
+           │
+           ▼
+   ┌───────────────────────────────┐
+   │ generateMerchantAuthUrl()      │
+   │ 或 generateStoreAuthUrl()      │
+   └───────────┬───────────────────┘
+               │
+               ▼
+      ┌────────────────────────┐
+      │ 重定向到闪送授权页面   │
+      └────────┬───────────────┘
+               │
+               ▼
+      ┌────────────────────────────┐
+      │ 用户填写闪送账号授权       │
+      └────────┬───────────────────┘
+               │
+               ▼
+      ┌──────────────────────────┐
+      │ 闪送重定向回 redirect_uri │
+      │ 并携带 code 和 shopId    │
+      └────────┬────────────────┘
+               │
+               ▼
+      ┌─────────────────────────────┐
+      │ getAccessToken($code)       │
+      └────────┬────────────────────┘
+               │
+               ▼
+      ┌──────────────────────────┐
+      │ 保存 token 到数据库      │
+      └─────────────────────────┘
+```
+
+## 🗄️ 数据库表设计参考
+
+建议创建表来存储授权信息:
+
+```sql
+CREATE TABLE `xh_shans_auth_token` (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `user_id` int(11) NOT NULL COMMENT '用户ID',
+  `shop_id` varchar(50) NOT NULL COMMENT '闪送商户ID',
+  `access_token` varchar(100) NOT NULL COMMENT '访问令牌',
+  `refresh_token` varchar(100) NOT NULL COMMENT '刷新令牌',
+  `expires_at` int(11) NOT NULL COMMENT 'token过期时间戳',
+  `auth_type` enum('all_store','single_store') NOT NULL DEFAULT 'all_store' COMMENT '授权类型',
+  `third_store_id` int(11) COMMENT '第三方门店ID',
+  `shans_store_id` varchar(50) COMMENT '闪送门店ID',
+  `created_at` timestamp DEFAULT CURRENT_TIMESTAMP,
+  `updated_at` timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_user_shop` (`user_id`, `shop_id`),
+  KEY `idx_expires_at` (`expires_at`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+```
+
+## 🔐 环境配置
+
+配置文件 (`common/config/params.php`) 中已包含:
+
+```php
+'shanAppId' => 'ss9QcKVXwvcMTkHzw',        // App Key
+'shanAppSecret' => '5ivRvyBXLrprGPssYUUvpKXwoZAojwDg',  // App Secret
+'expressShanUrl' => 'http://open.s.bingex.com',  // 测试环境
+// 或 'https://open.ishansong.com'  生产环境
+```
+
+环境自动识别:
+- 生产环境(YII_ENV=production): `https://open.ishansong.com`
+- 其他环境(YII_ENV=dev/test等): `http://open.s.bingex.com`
+
+## ⚠️ 重要注意事项
+
+### 1. **Token 生命周期管理**
+- AccessToken 有效期:30 天(2592000秒)
+- RefreshToken 长期有效
+- Code 有效期:1 分钟,只能使用 1 次
+
+### 2. **签名算法**
+- 算法:MD5
+- 签名包含的参数必须按字典序排序
+- 空值和 null 值需要过滤
+
+### 3. **URL 编码**
+- redirectUri 必须进行 URLEncode 编码
+- 该类自动处理 redirectUri 的编码
+
+### 4. **错误处理**
+所有方法返回统一的响应格式,包含 `success` 字段:
+
+```php
+// 成功响应
+['success' => true, 'access_token' => '...', ...]
+
+// 失败响应
+['success' => false, 'error' => '错误信息', ...]
+```
+
+### 5. **测试环境**
+- 使用测试账号进行授权测试
+- 需在闪送开放平台同步测试环境获取测试账号
+- 测试环境 URL:`http://open.s.bingex.com`
+
+### 6. **安全建议**
+- 妥善保管 `appSecret`,不要暴露在前端
+- State 参数建议使用用户ID或其他唯一标识
+- 定期验证和刷新 token
+- 实现 token 过期检测和自动刷新机制
+
+## 🚀 集成示例
+
+完整的集成示例可以参考项目中的:
+- `app-ghs/controllers/DeliveryController.php` - 快递相关的授权处理
+- `common/components/delivery/services/adapter/ShansongAdapter.php` - 闪送API调用示例
+
+## 📝 相关文档
+
+- 闪送开放平台:https://open.ishansong.com
+- 应用申请路径:登录闪送开放平台 -> 账号中心 -> 应用信息
+- 获取 AppKey 和 AppSecret:登录闪送开放平台 -> 账号中心 -> 应用信息
+
+---
+
+如有问题,请参考闪送官方文档或查看类中的详细代码注释。

BIN
任务小笔记/imgs/Pasted image 20251210143055.png


+ 6 - 1
任务小笔记/问题、优化.md

@@ -1 +1,6 @@
-- 无网络时,客户页疯狂快速加载 custom/list 接口
+- 无网络时,客户页疯狂快速加载 custom/list 接口
+- 
+- VIVO 消息分类配置
+![[Pasted image 20251210143055.png]]
+
+- 货拉拉配送优惠券

Unele fișiere nu au fost afișate deoarece prea multe fișiere au fost modificate în acest diff