标签归档:api

Laravel 实践 – Api Token认证

常见的Session认证中
1 客户端如果没有传递SessionID过来,那么就认为未认证
2 如果传递了SessionID过来,服务端根据SessionID定位Session,如果定位不到,那么就认为未认证
3 如果定位到了Session,但是Session中的认证标记不存在或无效,那么就认为未认证
4 Session中的认证标记存在并有效,认证通过

客户端没有传递SessionID过来,服务端一般会回写一个携带了SessionID的Cookie(意味着启动一个会话,也可能不回写,比如浏览静态资源),下次这个Cookie就会回传到过来,这样就能确定多个请求之间是同一个会话。然后再从Session中判断认证标志从而确定是否已认证。

如果客户端没有回传SessionID(通常是使用Cookie作为载体),一般服务端就会启动一个新会话,那么之前的会话就无法定位了,但是服务端会话数据还是可能存在。如果客户端回传了SessionID,服务端定位不到这个会话,说明已经长久不登录,服务端的会话已经被删除,这个时候也产生一个新的会话,另外就是如果定位到会话(未被删除),但是会话中没有认证标记,或有标记,但是时间已经超时,这时可以不产生新的会话(一般登录成功后会更换一个SessionID)。

这里面,我们是否可以把这个SessionID(不重复),直接对应一个用户,如果传递了SessionID,但是定位不到用户,认为未认证。如果定位到了用户,并且token未超时,认为认证通过。和Session方案相比,Session可以存储认证凭证,从而不用每次都查询。这里的简化流程,就是API Token认证。

在Laravel中,提供了TokenGuard,简单来说就是比对Token,具体操作流程:
1 在users表中添加api_token字段

$table->string('api_token', 60)->unique()->nullable();

2 在users表中寻找一个用户,把api_token字段填写一个任意token

3 在routes的api.php中定义一个闭包路由,应用auth:api中间件(auth表示需要认证,默认使用web对应的Guard,冒号后可以指定Guard)

Route::middleware('auth:api')->get('/user', function (Request $request) {
    return $request->user();
});

4 测试

curl -X http://demo.text/api/user \
-H "Accept: application/json" \
-d "token=xxxxxxxxxxxxxxxxxx"

注意:定义在api.php中的路由,访问时是不会产生会话的(确少StartSession中间件)。另外,Laravel的异常是根据请求是否是ajax或是否有Accept请求头并且包含json来渲染JSON返回的,否则就是渲染Html输出。但是API应该总是返回JSON,所以应该总是携带Accept: application/json请求头。不过每次都要这么写,多少有点麻烦,所以换一个姿势。API的URL都是以api开头的,所以可以修改$request的请求头,这个可以使用容器的rebinding来进行:

#文件:app/Providers/AppServiceProvider.php

    public function register()
    {
        $this->addAcceptableJsonType();
    }

    /**
     * Add "application/json" to the "Accept" header for the current request.
     */
    protected function addAcceptableJsonType()
    {
        $this->app->rebinding('request', function ($app, $request) {
            if ($request->is('api/*')) {
                $accept = $request->header('Accept');
                if (!str_contains($accept, ['/json', '+json'])) {
                    $accept = rtrim('application/json,' . $accept, ',');
                    $request->headers->set('Accept', $accept);
                    $request->server->set('HTTP_ACCEPT', $accept);
                    $_SERVER['HTTP_ACCEPT'] = $accept;
                }
            }
        });
    }

在用户登录后,让其在后台自己产生token,然后把token分发出去使用,这是一个很传统的做法。另外,token长期有效,会存在安全问题。可以改造一下,在登录成功后更换token,登出时擦除token。另外,还可以给token设置一个超时时间:

#文件:routes/api.php
// 登录(每次登录都更换token,登录相当于更换token,登录成功后返回用户实例,包含token)
// 请求体:email  password
Route::post('backend/login', 'API\Backend\Auth\LoginController@login');
// 注册(注册成功后返回用户实例,包含token)
// 请求体: name  email  password  password_confirmation
Route::post('backend/register', 'API\Backend\Auth\RegisterController@register');
// 重置密码 - 发送邮件
// 请求体: email
Route::post('backend/password/email', 'API\Backend\Auth\ForgotPasswordController@sendResetLinkEmail');
// 重置密码 - 输入新密码
// 请求体: token  email  password  password_confirmation
Route::post('backend/password/reset', 'API\Backend\Auth\ResetPasswordController@reset');

// Token保护的路由
// 添加请求头:Authorization  =>  "Bearer Token"
Route::group([
    'middleware' => ['auth:backend_api']
], function () {
    // 登出(撤销token),需要传递token
    Route::any('backend/logout', 'API\Backend\Auth\LoginController@logout');

    // 获取用户
    Route::get('backend/admin', function (Request $request) {
        return [
            'data' => $request->user()
        ];
    });
});

#文件 config/auth.php
#添加backend_api
<?php

return [

    'defaults' => [
        'guard' => 'web',
        'passwords' => 'users',
    ],

    'guards' => [
        'web' => [
            'driver' => 'session',
            'provider' => 'users',
        ],

        'backend' => [
            'driver' => 'session',
            'provider' => 'admins',
        ],

        'api' => [
            'driver' => 'token',
            'provider' => 'users',
        ],

        'backend_api' => [
            'driver' => 'token',
            'provider' => 'admins',
        ],
    ],

    'providers' => [
        'users' => [
            'driver' => 'eloquent',
            'model' => App\Models\User::class,
        ],

        'admins' => [
            'driver' => 'eloquent',
            'model' => App\Models\Admin::class,
        ],
    ],

    'passwords' => [
        'users' => [
            'provider' => 'users',
            'table' => 'password_resets',
            'expire' => 60,
        ],
        'admins' => [
            'provider' => 'admins',
            'table' => 'admin_password_resets',
            'expire' => 60,
        ],
    ],

];

这里把用户存储到admins表,密码找回存储到admin_password_resets表。拷贝一套Auth文件,针对性覆盖某些方法:

#文件: app/Http/Controllers/API/Backend/Auth/LoginController.php
<?php

namespace App\Http\Controllers\API\Backend\Auth;

use Illuminate\Http\Request;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Illuminate\Support\Facades\Auth;
use App\Http\Controllers\Controller;

class LoginController extends Controller
{
    /*
    |--------------------------------------------------------------------------
    | Login Controller
    |--------------------------------------------------------------------------
    |
    | This controller handles authenticating users for the application and
    | redirecting them to your home screen. The controller uses a trait
    | to conveniently provide its functionality to your applications.
    |
    */

    use AuthenticatesUsers;

    /**
     * Where to redirect users after login.
     *
     * @var string
     */
    protected $redirectTo = '';

    /**
     * Create a new controller instance.
     *
     * @return void
     */
    public function __construct()
    {
    }

    /**
     * 用户通过认证,返回JSON
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    protected function sendLoginResponse(Request $request)
    {
        //API调用,没有启动会话
        //$request->session()->regenerate();
        $this->clearLoginAttempts($request);
        $user = with($this->guard()->user())->makeToken();

        return response()->json([
            'data' => $user,
        ], 200);
    }

    /**
     * 取回需要验证的信息
     * 覆盖来自AuthenticatesUsers中的credentials方法
     *
     * @param  \Illuminate\Http\Request $request
     * @return array
     */
    public function logout(Request $request)
    {
        with($request->user())->freshToken();

        return response()->json(['data' => 'Token已经擦除'], 200);
    }

    protected function guard()
    {
        return Auth::guard('backend');
    }

    /**
     * 取回需要验证的信息
     * 覆盖来自AuthenticatesUsers中的credentials方法
     *
     * @param  \Illuminate\Http\Request $request
     * @return array
     */
    protected function credentials(Request $request)
    {
        return $request->only($this->username(), 'password') + ['is_active' => 1];
    }
}

#文件: app/Http/Controllers/API/Backend/Auth/RegisterController.php
<?php

namespace App\Http\Controllers\API\Backend\Auth;

use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Validator;
use Illuminate\Foundation\Auth\RegistersUsers;
use Illuminate\Auth\Events\Registered;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Models\Admin;

class RegisterController extends Controller
{
    /*
    |--------------------------------------------------------------------------
    | Register Controller
    |--------------------------------------------------------------------------
    |
    | This controller handles the registration of new users as well as their
    | validation and creation. By default this controller uses a trait to
    | provide this functionality without requiring any additional code.
    |
    */

    use RegistersUsers;

    /**
     * Where to redirect users after registration.
     *
     * @var string
     */
    protected $redirectTo = '';

    /**
     * Create a new controller instance.
     *
     * @return void
     */
    public function __construct()
    {
    }

    /**
     * Get a validator for an incoming registration request.
     *
     * @param  array  $data
     * @return \Illuminate\Contracts\Validation\Validator
     */
    protected function validator(array $data)
    {
        return Validator::make($data, [
            'name' => 'required|string|max:255',
            'email' => 'required|string|email|max:255|unique:users',
            'password' => 'required|string|min:6|confirmed',
        ]);
    }

    /**
     * Create a new user instance after a valid registration.
     *
     * @param  array  $data
     * @return \App\Models\Admin
     */
    protected function create(array $data)
    {
        return Admin::create([
            'name' => $data['name'],
            'email' => $data['email'],
            'password' => bcrypt($data['password']),
        ]);
    }

    protected function guard()
    {
        return Auth::guard('backend');
    }

    /**
     * Handle a registration request for the application.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function register(Request $request)
    {
        $this->validator($request->all())->validate();

        event(new Registered($user = $this->create($request->all())));

        //不需要登录
        //$this->guard()->login($user);

        return response()->json([
            'data' => Admin::find($user->id)->makeToken(),
        ], 200);
    }
}

#文件: app/Http/Controllers/API/Backend/Auth/ForgotPasswordController.php
<?php

namespace App\Http\Controllers\API\Backend\Auth;

use Illuminate\Foundation\Auth\SendsPasswordResetEmails;
use Illuminate\Support\Facades\Password;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;

class ForgotPasswordController extends Controller
{
    /*
    |--------------------------------------------------------------------------
    | Password Reset Controller
    |--------------------------------------------------------------------------
    |
    | This controller is responsible for handling password reset emails and
    | includes a trait which assists in sending these notifications from
    | your application to your users. Feel free to explore this trait.
    |
    */

    use SendsPasswordResetEmails;

    /**
     * Create a new controller instance.
     *
     * @return void
     */
    public function __construct()
    {
    }

    /**
     * Get the response for a successful password reset link.
     *
     * @param  string  $response
     * @return \Illuminate\Http\Response
     */
    protected function sendResetLinkResponse($response)
    {
        return response()->json([
            'data' => trans($response),
        ], 200);
    }

    /**
     * Get the response for a failed password reset link.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  string  $response
     * @return \Illuminate\Http\Response
     */
    protected function sendResetLinkFailedResponse(Request $request, $response)
    {
        return response()->json([
            'message' => '发生错误',
            'errors' => ['email' => trans($response)],
        ], 500);
    }

    /**
     * 验证邮件地址
     *
     * @param  \Illuminate\Http\Request  $request
     * @return void
     */
    protected function validateEmail(Request $request)
    {
        $this->validate($request, ['email' => 'required|email']);
    }

    public function broker()
    {
        return Password::broker('admins');
    }
}


#文件: app/Http/Controllers/API/Backend/Auth/ResetPasswordController.php
<?php

namespace App\Http\Controllers\API\Backend\Auth;

use Illuminate\Support\Str;
use Illuminate\Foundation\Auth\ResetsPasswords;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Password;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Auth\Events\PasswordReset;
use App\Http\Controllers\Controller;

class ResetPasswordController extends Controller
{
    /*
    |--------------------------------------------------------------------------
    | Password Reset Controller
    |--------------------------------------------------------------------------
    |
    | This controller is responsible for handling password reset requests
    | and uses a simple trait to include this behavior. You're free to
    | explore this trait and override any methods you wish to tweak.
    |
    */

    use ResetsPasswords;

    /**
     * Where to redirect users after resetting their password.
     *
     * @var string
     */
    protected $redirectTo = '';

    /**
     * Create a new controller instance.
     *
     * @return void
     */
    public function __construct()
    {

    }

    /**
     * 如果账户已经禁用,不能再重置密码
     *
     * @param  \Illuminate\Http\Request  $request
     * @return array
     */
    protected function credentials(Request $request)
    {
        return $request->only(
            'email', 'password', 'password_confirmation', 'token'
        ) + ['is_active' => 1];
    }

    /**
     * 重置密码,重置后不需要重新登录
     *
     * @param  \Illuminate\Contracts\Auth\CanResetPassword  $user
     * @param  string  $password
     * @return void
     */
    protected function resetPassword($user, $password)
    {
        $user->password = Hash::make($password);

        $user->setRememberToken(Str::random(60));

        $user->save();

        event(new PasswordReset($user));

        //$this->guard()->login($user);
    }

    /**
     * Get the response for a successful password reset.
     *
     * @param  string  $response
     * @return \Illuminate\Http\Response
     */
    protected function sendResetResponse($response)
    {
        return response()->json([
            'data' => trans($response),
        ], 200);
    }

    /**
     * Get the response for a failed password reset.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  string  $response
     * @return \Illuminate\Http\Response
     */
    protected function sendResetFailedResponse(Request $request, $response)
    {
        return response()->json([
            'message' => '发生错误',
            'errors' => ['email' => trans($response)],
        ], 500);
    }

    public function broker()
    {
        return Password::broker('admins');
    }

    protected function guard()
    {
        return Auth::guard('backend');
    }
}

发生错误时,状态码返回500,成功操作时状态码返回200,验证不通过状态码返回422,登录错误超过阈值状态码返回423,没有权限状态码返回401。

另外,返回的格式:

// 状态码
[
    'message' => '', // 成功时可不返回
    'data' => [],    // 成功时返回的数据
    'errors' => []   // 不成功时,返回的错误详细
]

Paypal API

Paypal提供了两套API,REST API和NVP/SOAP API。REST API没有完全覆盖NVP API的功能,NVP API历史久远,未来应该会被REST API替换。

REST API使用OAuth2标准,首先需要有一个开发者账户,然后在开发者账户中创建APP(产生client_id, client_secret),Paypal账户授权给这个APP。

开发者网站:http://developer.paypal.com,然后需要注册一个真实的Paypal账户(可以个人,也可以商用),拿这个账户作为开发者账户去登录,然后就可以创建APP,分两个环境:正式和测试。如何创建一个测试APP,那么会自动给你创建一个测试商家账户,还有一个买家账户。当然也可以自己建多个测试账户。

——————————————————————-
在REST API之前,只有NVP API。为了可以使用NVP API,需要到自己的Paypal中的API设置中取到相关API签名(API用户名和密码,以及签名),只要暴露了这三个信息,就相当于是开放了自己的账户。

每个Paypal可以作为一个第三方,其它的Paypal账户如果把某些权限赋给了它,它就可以扮演第一方的身份去获取信息。关于权限赋予的流程,官方文档有详细描述。而Paypal后台的第三方许可,实际对应这个操作,最终都是赋予第三方权限。

这里需要输入的就是第三方Paypal账户的API的用户名(每个账户中的API签名部分),点击查找后会让你选择哪些权限赋予这个第三方:

结论:Paypal可以通过API签名开放账户,也可以把权限授予其它账户,由其它账户(第三方)代理访问。

<?php

namespace Paypal;

class Api
{
    protected $username = '';

    protected $password = '';

    protected $signature = '';

    protected $version = '95.0';

    protected $endPoint = 'https://api-3t.paypal.com/nvp';

    protected $subject = '';

    public function __construct($username, $password, $signature, $subject = '', $sandbox = false)
    {
        $this->username = $username;
        $this->password = $password;
        $this->signature = $signature;
        if (!empty($subject)) {
            $this->subject = trim($subject);
        }
        if ($sandbox) {
            $this->endPoint = 'https://api-3t.sandbox.paypal.com/nvp';
        }
    }

    // 作为第三方访问Paypal
    public function setSubject($email)
    {
        $this->subject = trim($email);
    }

    // 取回交易列表
    //$params = [
    //    'STARTDATE' => $startTime,
    //    'ENDDATE' => $endTime,
    //    'RECEIVER' => '',
    //    'TRANSACTIONCLASS' => 'All'
    //];
    public function getTransactions(array $params)
    {
        $return = ['success' => 0, 'message' => '', 'data' => []];
        if (empty($params['STARTDATE']) || empty($params['ENDDATE'])) {
            $return['message'] = '参数不合法';
            return $return;
        }

        $result = $this->post('TransactionSearch', $params);
        if (false === $result) {
            $return['message'] = 'CURL请求异常';
            return $return;
        }

        $tarr = explode('&', $result);
        $data = [];
        foreach ($tarr as $item) {
            $tmp = explode('=', rawurldecode($item));

            preg_match('/^L_([a-zA-Z\_]+)([0-9]+)/', $tmp[0], $m);
            if (isset($m[0]) && isset($m[1]) && isset($m[2])) {
                $data[$m[1]][$m[2]] = trim($tmp[1]);
            } else {
                $data[$tmp[0]] = trim($tmp[1]);
            }
        }

        // ACK 等于 Warming时,数据返回不齐全(Paypal每次查询最多返回100条)
        if (empty($data['ACK']) || ($data['ACK'] == 'Failure')) {
            $return['message'] = "API调用ACK返回Failure";
        } else {
            $return['success'] = 1;
            $return['data'] = $data;
        }

        return $return;
    }
 
    // 通用封装
    protected function post($api, array $params)
    {
        $global = [
            'METHOD' => $api,
            'VERSION' => $this->version,
            'USER' => $this->username,
            'PWD' => $this->password,
            'SIGNATURE' => $this->signature
        ];
        if (!empty($this->subject)) {
            $global['SUBJECT'] = $this->subject;
        }

        return $this->doRequest($this->endPoint, array_merge($global, $params));
    }

    // CURL请求
    protected function doRequest($url, $data)
    {
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_HEADER, '');
        curl_setopt($ch, CURLOPT_URL, trim($url));
        curl_setopt($ch, CURLOPT_HEADER, false);
        curl_setopt($ch, CURLOPT_TIMEOUT, 90);
        curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 20);
        if (!empty($data)) {
            if (is_array($data)) {
                curl_setopt($ch, CURLOPT_POST, true);
                curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
            } elseif (is_string($data)) {
                curl_setopt($ch, CURLOPT_POST, true);
                curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
            }
        }
        if (\PHP_OS === 'WINNT') {
            curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
            curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
        }
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false);
        curl_setopt($ch, CURLOPT_USERAGENT, "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:21.0) Gecko/20100101 Firefox/21.0");
        $result = curl_exec($ch);
        $error = curl_errno($ch);
        curl_close($ch);
        if ((int)$error == 0) {
            return $result;
        }
        return false;
    }
}

Wish的OAuth 2认证流程

Wish还提供了一个sandbox环境,不像那些个Ali系统,从不提供这种东西,想想都恶心(玛尼,就知道坑钱去了)。

http://sandbox.merchant.wish.com,自己注册个sandbox账户,随意折腾。

其实,OAuth 2认证流程都差不多,首先跳转到认证登录页,要求输入用户名密码进行授权(这个过程可能有点差别),然后重定向到预定的redirect_uri并附上一个一次性的code(这个就是认证码了),然后用这个code去获取access_token和refresh_token,这个时候要把它们保存下来,再然后就可以使用access_token来访问了。如果access_token过期了,可以使用refresh_token来换取新的access_token。

在进行第一步之前,要先获取API相关信息(账户-设置):
wish_api
理论上,这里就是对应一个App,那么不同的店铺应该都可以绑定到这个App。不过,如果别人都可以绑定到你的App,那么你就可以提供服务了,很明显,它不应该不允许你这样干,这个跟恶心的Ali系统一样,如果要这样干,需要得到特别授权,所以,这里的App(Api)跟你的账户一一对应,你的账户只能授权给你自己创建的这个App。(注意以上的信息,后面都要用到)

在基本信息中,还有一个叫商户 ID的,它唯一标识你的账户。这个后面会涉及到。

弄好之后,就可以开始了。详细内容参考:https://merchant.wish.com/documentation/oauth。这里只做一些备注。在第一步时,直接发起https://merchant.wish.com/oauth/authorize?client_id={client_id},参数除了client_id,你无法再提供其它参数了,有些实现方案还可以在这传递redirect_uri和其它参数,Wish不允许,授权码(code)直接附加到你在账户设置里面设置的那个redirect_uri。

在第二步中,你需要使用第一步中获取到的一次性code换取令牌,你需要通过POST方法发送如下参数:

//https://merchant.wish.com/api/v2/oauth/access_token 
client_id	Your app's client ID
client_secret	Your app's client secret
code	        The authorization code you received
grant_type	The string 'authorization_code'
redirect_uri	Your app's redirect uri that you specified when you created the app

这里需要注意的是,redirect_uri必须填写在你账户里设置的那个,否则无法通过验证。在Ali系统中,这个是不验证的,可以随意,玛尼,被它坑死。

这个通过之后会返回一个JSON信息,包含了Token,这个时候要把Token保存起来。不过这里,如果参考文档,你发现,它返回的数据没带有商户Id,那么怎么跟商户关联起来?你要是可以在redirect时携带参数还好,不过Wish你允许啊,所以,千万别被它坑了,真实返回的JSON是带有这个商户ID,这样就可以对应起来。

从真实返回的信息可以知道,它的Access Token有效是30天(玛尼,是不是有点长),另外,refresh_token视乎没有过期概念。在Access Token过期前换一个新的,不知道旧的会不会失效。同样,如果重新对App进行授权,这样就产生新的refresh_token,那么旧的会不会就失效了呢? 答案应该是的。(Ali系貌似这个问题比较隐晦)所以,如果要换一换Token,再来一次授权吧。

最后说说官方提供的PHP SDK吧。地址:https://github.com/ContextLogic/Wish-Merchant-API,可以通过composer引用,不过要注意的是,它至今还是dev版本,所以要这样干:

{
  "minimum-stability": "dev",
  "require":{
      "wish/php-sdk":"*"
  }
}

好的不教,这个简直要害死你。这样其它的包,也下载dev分支的给你。对我来说,我直接下载下来当本地包使用,因为这个包有点问题,需要改。故而:

    "autoload": {
        "psr-4": {
            "Wish\\": "local/Wish/"
        }
    },

然后更新下就完事了。

我第一个需要改这个包的地方就是,它使用了curl去发起请求,而Wish的Api都是https的,玛尼,Windows下curl无法验证https的证书(需要设置,指定CA证书的位置),所以我需要把它给位不验证:

//WishRequest.php 65行
if ((int)preg_match('/^HTTPS/i', $url) > 0) {
    $options[CURLOPT_SSL_VERIFYPEER] = false;
    $options[CURLOPT_SSL_VERIFYHOST] = false;
}

另外,这个鬼佬写得代码是用两个空格作为tab的,简直欠抽的节奏。还好格式化工具比较强大。

然后,打开WishSession.php,兼容问题:

private static $api_key;
private static $session_type;
private static $merchant_id;

#改为

private $api_key;
private $session_type;
private $merchant_id;

在实例方法中,出现和静态变量重名报错。以下是一段实例:

// Laravel 的Model
<?php
namespace App;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Wish\WishAuth;

class WishAccount extends Model
{

    protected $table = 'wish_account';

    protected $guarded = [];

    public function getAccessToken($type='prod')
    {
        $default = ['success'=>0,'message'=>'获取Access Token失败','access_token'=>''];
        // Access Token不为空,判断是否过气
        if (! empty($this->access_token)) {
            $timeOut = trim($this->access_token_timeout_at);
            if(!empty($timeOut) && (strtotime($timeOut) > time())) {
                return ['success'=>1,'message'=>'','access_token'=>$this->access_token];
            }
        } 
        
        // 更换新Token
        if(! empty($this->refresh_token)) {
            $client_id = trim($this->client_id);
            $client_secret = trim($this->secret_id);
            
            if(empty($client_id) || empty($client_secret)) {
                $default['message'] = "Access Token过期,使用Refresh Token换取Access Token失败(Client_id 或 Client_secret为空,无法换取Token)";
                return $default;
            }
            
            // 
            try {
                $auth = new WishAuth($client_id,$client_secret,$type);
                $response = $auth->refreshToken($this->refresh_token);
                if($response->getStatusCode() > 0) {
                    $default['message'] = "Access Token过期,使用Refresh Token换取Access Token失败(状态码:".$response->getStatusCode()." Msg:".$response->getMessage();
                } else {
                    // 返回的Refresh Token,跟之前一样
                    $this->freshToken($response->getData());
                    return ['success'=>1,'message'=>'','access_token'=>$response->getData()->access_token];
                }
            } catch (Exception $e) {
                $default['message'] = "Access Token过期,使用Refresh Token换取Access Token失败(状态码:4000 Msg:Unauthorized access)";
                return $default;
            }
        }
        
        return $default;
    }
    
    // 一次性code换取Token access_token和refresh_token
    public function setToken($code,$type='prod') {

        $client_id = trim($this->client_id);
        $client_secret = trim($this->secret_id);
        $redirect_uri = trim($this->redirect_uri);
        
        if(empty($client_id) || empty($client_secret) || empty($redirect_uri) || empty($code)) {
            return "信息不完整,无法完成授权。";
        }
        
        try {
            $auth = new WishAuth($client_id,$client_secret,$type);
            $response = $auth->getToken($code,$redirect_uri);
            
            $this->freshToken($response->getData());
        } catch (Exception $e) {
            return "4000 或 1016 异常。";
        }
    }
    
    // 保存Token信息
    protected function freshToken($data) {
        if (is_object($data)) {
            $this->refresh_token = $data->refresh_token;
            $this->refresh_token_timeout_at = date("Y-m-d H:i:s", time() + 1 * 12 * 30 * 24 * 3600);
            $this->access_token = $data->access_token;
            $this->access_token_timeout_at = date("Y-m-d H:i:s", time() + $data->expires_in - 2 * 24 * 3600);
            
            $this->save();
        }
    }
}

抓Listing:

use Wish\WishClient;

$access_token = 'an_example_access_token';

$client = new WishClient($access_token,'prod');
$products = $client->getAllProducts();
echo "You have ".count($products)." products!\n";

注意,这段代码中间是可能抛出异常的,比如Access Token过期等,需要注意捕捉。

eBay API 之 Feedback

一个eBay账户可以卖东西也买东西,分别扮演卖家和买家。eBay对每一个产品的付款称为一个交易,如果购买了多个产品就有多次交易。eBay的Feedback是针对交易的(不是针对订单),这个必须清楚。作为买家和卖家,都可以给对方留Feedback和接收对方的Feedback(作为卖家接收Feedback,作为买家接收Feedback),也可以回复对方的Feedback,针对回复,作为买卖家,还可以Followup。

以下分开解释:
作为卖家,买家留评(卖家接收到评价),卖家针对买家的评价(好中差评)进行回复,买家收到回复后还可以辩解一下,针对回复来一次Followup。

作为买家,卖家留评(买家接收到评价),买家针对卖家的评价(好中差评)进行回复,卖家收到回复后还可以辩解一下,针对回复来一次Followup。

作为卖家,一般会在付款完成之后就会给买家好评,买家一般都不会回复这个评价,真正关心评价的是作为卖家,所以只需要实现卖家这边的功能即可,虽然eBay的Feedback API实现了双向交互。

那么流程就是如下:
1 以卖家的身份获取评价(买家留评,卖家接收到评价)

<?xml version="1.0" encoding="utf-8"?> 
<GetFeedbackRequest xmlns="urn:ebay:apis:eBLBaseComponents"> 
  <DetailLevel>ReturnAll</DetailLevel> 
  <FeedbackType>FeedbackReceivedAsSeller</FeedbackType> 
  <CommentType>Positive</CommentType> 
  <RequesterCredentials> 
    <eBayAuthToken>SellerToken</eBayAuthToken> 
  </RequesterCredentials> 
</GetFeedbackRequest>

CommentType用来过滤好中差评,FeedbackType用来过滤接收到的评价的接收实体是卖家还是买家。如果不区分好中差评,去掉CommentType过滤条件即可。如果不区分接收实体是卖家还是买家,去掉FeedbackType过滤条件即可。实际上,返回的具体的Feedback中,有一个Role的字段,用来表明这个Feedback接收主体,一般应该使用FeedbackType来过滤Feedback,返回结果的Role字段可以直接忽略它,因为我们是做卖家工具,不是做买家工具,如果是做买家工具,获取卖家给买家的Feedback才有意义。

2 回复买家给的评价(一般是在中差评时回复)
参考文档(就发送一条信息)。

关于Feedback同步,GetFeedback并不支持时间段过滤,最新的评价总会排在GetFeedback结果的前面,所以只要每天同步前面几页Feedback即可,Feedback一旦下载,回复的时候可以把回复的信息也写入到本地,这样就相当于本地和远程都同步了。

eBay的Feedback还有以下更细的内容,比如评价是否被屏蔽等,可以参考API文档。

eBay用户授权流程

ebay-developer
往下翻,点击Customize the eBay User Consent Form,找到Manage Your RuNames部分,然后点击Generate Runame,这样就会产生一个所谓的RuName,长成这个样子”vfeelit-vfeelit1d-65eb–tboemfhvb”,可以点击多次产生多个RuName,不过看起来没有什么必要,那么RuName是什么毛呢,先看看针对它的配置吧,在对应字符串右边点击Show Details,将展示如下表单:
eBay RuName
Display Title和Display Description是展示给用户看的标题和描述,这个信息可以在Application Level Settings中设置Show Application Details为enabled或disabled来设置,还可以上传Logo,这些信息是在用户点击同意授权时展示的出来给用户看的。

Token Return Method设置如何获取Token,Authorization Type为授权类型,Accept Redirect URL为成功授权后跳转到的地址,Reject Redirect URL授权被拒绝时跳转到的地址。实际上这里的值只要默认就可以了,关于Accept Redirect URL和Reject Redirect URL如果要设置,必须是HTTPS的链接地址,实际设置这两个变量毫无意义,因为它不会回传任何信息到这两个URL。所以,RuName实际上是一个Application的标识符(别名),它设置了相关的认证类型,Token获取方式等等,比如当用户授权成功后,就需要用这里设置的Token Return Method来获取Token。

对于一个应用,只要设置一个RuName即可。多个RuName也支持。这个步骤完成后应用就支持多用户授权了。

授权过程,步骤如下:
1 调用GetSessionID获取一个SessionID
这个API调用详细参考:http://developer.ebay.com/Devzone/XML/docs/Reference/ebay/GetSessionID.html,这里面如果是发送XML,只要发送RuName即可:

<?xml version="1.0" encoding="utf-8"?>
<GetSessionIDRequest xmlns="urn:ebay:apis:eBLBaseComponents">
  <!-- Call-specific Input Fields -->
  <RuName> string </RuName>
  <!-- Standard Input Fields -->
  <ErrorLanguage> string </ErrorLanguage>
  <MessageID> string </MessageID>
  <Version> string </Version>
  <WarningLevel> WarningLevelCodeType </WarningLevel>
</GetSessionIDRequest>

从返回提取SessionID即可。

2 用获取到的SessionID构建URL
URL格式:https://signin.ebay.com/ws/eBayISAPI.dll?SignIn&RUName=RUName&SessID=SessionID,SessionID需要经过URL-encoded,然后定位到这个URL,这样将打开用户登录表单,用户登录成功后将跳到一个是否同意授权的页面:
eBay 用户登录
登录后跳到:
授权应用
这个的Grant application access后的名称就是设置RuName是指定的Display Title,这就说明RuName可以看做是Application。最下面展示的是应用的信息,这个可以在开发者账户中进行设置。点击I agree跳转到如下页:
ebay_auth_success
这个也是可以设置的。前面已经论述。这里叫你去关闭这个页。TMD,这就完了,然后接下来要发起获取Token的操作,这个过程明显让我们感觉整个流程被中断了。我们观察一下这个返回的URL:https://signin.ebay.com/ws/eBayISAPI.dll?ThirdPartyAuthSucessFailure&isAuthSuccessful=true&ebaytkn=&tknexp=1970-01-01+00%3A00%3A00&username=testuser_vfeelit,问号之前的是可以设置的(需要HTTPS),后面的数据是固定的,isAuthSuccessful参数表明是否成功,username指出了eBay用户名,应该只要设置一下应用的返回地址,根据这些参数,也是可以自动获取Token的(流程不中断,因为获取到了username)

3 获取Token
以上两个步骤完成后,只是说明用户对应用进行了授权,但是授权码应用程序还没有获取到,这个时候调用FetchToken(传递eBay ID),就能返回针对这个账户的Token。

4 保存这个Token和有效时间
Token保存起来后就可以使用这个Token来访问API操作对应账户数据了。

这个过程看起来并没有比oAuth 2先进多少,这个流程过程中的中断让人产生困惑,虽然可以设置Accept Redirect URL,但是它要求是HTTPS的链接。

Ebay E邮宝API开发

开发者专区(总入口)
http://www.ebay.cn/developer/
国际e邮宝API V3
http://www.ebay.cn/developer/single/epacket.html
国际e邮宝API V4 (eBay亚太物流平台API)
http://www.ebay.cn/developer/single/APAC-SHIPPING.html
说明:V3还可用,以后全面转换成V4(原计划是2014-10-10,后推迟)

1 注册开发者账户
https://developer.ebay.com/devzone/account
在My Account页获取生成一组Key(分开发环境 和 生产环境)

2 注册Sandbox账户
打开www.sandbox.ebay.com,点击Get Started下的eBay Sandbox User Registration链接,会跳转到developer.ebay.com,要求用开发者账户先登录,登录后跳到eBay Sandbox User Registration Tool开始注册账户,用户名统一以TESTUSER_开头,注册类型只有Buyer and Seller,说明既可以是买家,也可以是卖家,也可以再次注册一个账户,分别模拟买家卖家。

也可以登录开发者账户后在tools下点击Sandbox User Registration链接进行Sandbox账户注册。

3 产生User Token
实际就是eBay用户对开发者或APP的授权码,可以在开发账户中的Tools下面点击Get a User Token获取,点击后跳转到key选择页面,输入(或选择环境或Key),然后点击Continue to generate token,然后跳转到授权登录页面,使用之前支持的账户进行登录(表示这个账户授权到开发者账户),输入账户密码登录后跳到一个授权提醒页面,点击I agree,然后弹出结果,点击Save Token。

4 使用对应的授权码访问API(操作授权码对应的店铺)

如果刚开始接触API开发,往往在账户授权这里被卡住,有点难理解。实际上,这里申请的开发者账户可以看做是一个应用程序(至少是代表),现在这个应用程序要获取你账户(eBay账户或eBay账户对应的EUB)里面的信息,这里就涉及到两个问题。第一,数据如何访问,第二,如何授权这个应用程序访问这些个人数据。对第一个问题,自然是通过提供API访问了,但是访问前必须先获取授权,这就是第二个问题。关于授权,业界有成熟的解决方案OAuth 2,它用得非常普遍。但是eBay没有采用OAuth 2,它自己实现了一套授权逻辑(不过跟OAuth 2也有类似地方),过程这里先跳过了,最终结果是eBay卖家输入了它的账户密码点击同意授权,应用程序将接收到一个很长的字符串,美其名曰Token。应用程序在使用API时,这个Token是必须传递的,Token是有有效期的,会不会导致它泄露呢,理论是不会的,因为它作为POST数据的一部分通过HTTPS进行传递。就算泄露了Token,要访问API时还要知道AppKey已经对应的签名等。

以下是一段来自官方的例子:

error_reporting(E_ALL);
$compatabilityLevel = 717;    // eBay API version

$devID = "95a9c0d-1cad-4fda-b74d-b610efbb560";
$appID = "EBTCo63ba-b11-4e96-b0c3-b4dd064239";
$certID = "f2fd3c8-18d4-4419-8fd3-72ae811829f";
$serverUrl = "https://api.sandbox.ebay.com/ws/api.dll";
$userToken = "xxxxxxxxxxxxxx";

$siteID = 0;
//要调用的API
$verb = 'GetTokenStatus';
 
$headers = array (
    'X-EBAY-API-COMPATIBILITY-LEVEL: ' . $compatabilityLevel,
    'X-EBAY-API-DEV-NAME: ' . $devID,
    'X-EBAY-API-APP-NAME: ' . $appID,
    'X-EBAY-API-CERT-NAME: ' . $certID,
             
    //the name of the call we are requesting
    'X-EBAY-API-CALL-NAME: ' . $verb,           
             
    //SiteID must also be set in the Request's XML
    //SiteID = 0  (US) - UK = 3, Canada = 2, Australia = 15, ....
    //SiteID Indicates the eBay site to associate the call with
    'X-EBAY-API-SITEID: ' . $siteID,
);
 
//POST的数据,一个XML字符串
$requestXmlBody = '<?xml version="1.0" encoding="utf-8"?>
<GetTokenStatusRequest xmlns="urn:ebay:apis:eBLBaseComponents">
  <RequesterCredentials>
    <eBayAuthToken>'.$userToken.'</eBayAuthToken>
  </RequesterCredentials>
</GetTokenStatusRequest>';
 
//使用CURL发送数据        
//initialise a CURL session
$connection = curl_init();
//set the server we are using (could be Sandbox or Production server)
curl_setopt($connection, CURLOPT_URL, $serverUrl);
         
//stop CURL from verifying the peer's certificate
curl_setopt($connection, CURLOPT_SSL_VERIFYPEER, 0);
curl_setopt($connection, CURLOPT_SSL_VERIFYHOST, 0);
         
//set the headers using the array of headers
curl_setopt($connection, CURLOPT_HTTPHEADER, $headers);
         
//set method as POST
curl_setopt($connection, CURLOPT_POST, 1);
         
//set the XML body of the request
curl_setopt($connection, CURLOPT_POSTFIELDS, $requestXmlBody);
         
//set it to return the transfer as a string from curl_exec
curl_setopt($connection, CURLOPT_RETURNTRANSFER, 1);
         
//Send the Request
$response = curl_exec($connection);
         
//close the connection
curl_close($connection);
 
header("Content-type: text/xml");
print_r($response);  

返回的XML:

<GetTokenStatusResponse><Timestamp>2014-11-19T15:16:41.219Z</Timestamp><Ack>Success</Ack><Version>893</Version><Build>E893_CORE_API_17097905_R1</Build><TokenStatus><Status>Active</Status><EIASToken>nY+sHZ2PrBmdj6wVnY+sEZ2PrA2dj6wFk4GhDJmKogudj6x9nY+seQ==</EIASToken><ExpirationTime>2016-05-12T15:04:46.000Z</ExpirationTime></TokenStatus></GetTokenStatusResponse>

EBay Api顺利通过沙盒测试,但是我这里的E邮宝V4.0.0沙盒测试一直失败,起初以为是账户等信息不对,换了几次,总是提醒Token无效,但是通过EBay Api测试Token是有效的(如上代码运行结果)。后来我直接到Ebay香港注册了个真实的Ebay账户,然后授权到真实的开发者账户,测试就通过:

$serverUrl="https://api.apacshipping.ebay.com.hk/aspapi/v4/ApacShippingService";

$request=array();
$request["APIDevUserID"]="xxxxx";
$request["APISellerUserToken"]="00000";
$request["APISellerUserID"]="xxxxx";
$request["AppID"]="EBTCo1d7-1e69-4cbf-adbf-7c47209ab";
$request["AppCert"]="45d1d5c-d54c-4381-bd3b-f9b0949479";
$request["MessageID"]="";
$request["Version"]="4.0.0";
$request["Carrier"]="CNPOST";
//$request["Service"]="EPACK";

$client = new SoapClient($serverUrl."?wsdl");
$r = $client->VerifyAPACShippingUser(array("VerifyAPACShippingUserRequest"=>$request));

print_r($r);

///输出
stdClass Object
(
    [VerifyAPACShippingUserResult] => stdClass Object
        (
            [Version] => 4.0.0
            [Ack] => Success
            [Message] => VerifyAPACShippingUser succeeded
            [Timestamp] => 2014-11-20T06:58:05.989-07:00
            [InvocationID] => F4C3C334D173429BA38538986C620B1D
            [CarrierList] => stdClass Object
                (
                    [CarrierGeo] => stdClass Object
                        (
                            [Carrier] => CNPOST
                            [FromCountryCode] => CN
                        )

                )

        )

)

看起来,E邮宝v4.0.0服务还有待完善。

附加信息,E邮宝实际是一个独立的服务,是要注册账户的,你可以使用你的Ebay账户(仅限香港注册的)去注册一个E邮宝账户,这样你的这个EBay账户也就是E邮宝账户,同时你的Ebay账户默认会被添加到管理账户的卖家列表中,你可以继续添加Ebay账户进来,就是一个E邮宝账户对应多个Ebay账户,在调用E邮宝API时,参数APISellerUserID是必填的,它是能定位到E邮宝的中设置的的Ebay账户。

只要通过Ebay的API认证,就能调用E邮宝的API管理物流发货。

Magento API 管理产品图片

Magento API操作产品图片参考文档:http://www.magentocommerce.com/api/soap/catalog/catalogProductAttributeMedia/productImages.html

1 首先看看所谓的MediaTypes:

// 44 是属性集Id
$result = $proxy->catalogProductAttributeMediaTypes($sessionId, '44');
var_dump($result); 

// 输出
array(4) {
  [0]=>
  object(stdClass)#2 (2) {
    ["code"]=>
    string(9) "thumbnail"
    ["scope"]=>
    string(5) "store"
  }
  [1]=>
  object(stdClass)#3 (2) {
    ["code"]=>
    string(11) "small_image"
    ["scope"]=>
    string(5) "store"
  }
  [2]=>
  object(stdClass)#4 (2) {
    ["code"]=>
    string(5) "image"
    ["scope"]=>
    string(5) "store"
  }
  [3]=>
  object(stdClass)#5 (2) {
    ["code"]=>
    string(14) "my_media_image"
    ["scope"]=>
    string(5) "store"
  }
}

可见媒体的类型就是指thumbnail、small_image、image和my_media_image(自定义属性)。自定义属性my_media_image添加到了属性集Id为44的某个组中,我们找一个应用了这个属性集的产品查看图片:
Magento添加自定义的媒体属性

除了系统自定义的类型,还添加了一个my_media_image(底下的单选表示选择哪张图的链接保存到这个属性中)。

2 catalogProductAttributeMediaCurrentStore
这个方法可以获取商店Id, 或者设置当前商店Id(方法第二参数)。可以通过它改变当前商店(默认情况在处于0)方便针对某个商店的图片应用。

$result = $proxy->catalogProductAttributeMediaCurrentStore($sessionId, 'english');
var_dump($result);

3 catalogProductAttributeMediaInfo
获取媒体信息,通过第三参数指定图片链接。看起来当只想查看某个图片的信息时比较有用。但是还要指定图片链接….

4 catalogProductAttributeMediaRemove
这个方法和catalogProductAttributeMediaInfo参数类似,第三参数指定图片链接,它将删除这个图片(注意,它只是把相关信息从数据库中去除,并没有实际删除文件)。

5 catalogProductAttributeMediaList
获取图片列表,第二参数指定产品Id,第三参数可选,指定StoreId(默认是0,表示默认,如果要获取只应用到了某个商店的图片列表就需要指定了)

$result = $proxy->catalogProductAttributeMediaList($sessionId, '48');
var_dump($result);

array(3) {
  [0]=>
  object(stdClass)#2 (6) {
    ["file"]=>
    string(48) "/k/o/kodak-easyshare-c530-5mp-digital-camera.jpg"
    ["label"]=>
    string(0) ""
    ["position"]=>
    string(1) "0"
    ["exclude"]=>
    string(1) "0"
    ["url"]=>
    string(96) "http://magento.vfeelit.com/media/catalog/product/k/o/kodak-easyshare-c530-5mp-digital-camera.jpg"
    ["types"]=>
    array(0) {
    }
  }
  [1]=>
  object(stdClass)#3 (6) {
    ["file"]=>
    string(50) "/k/o/kodak-easyshare-c530-5mp-digital-camera-1.jpg"
    ["label"]=>
    string(0) ""
    ["position"]=>
    string(1) "0"
    ["exclude"]=>
    string(1) "0"
    ["url"]=>
    string(98) "http://magento.vfeelit.com/media/catalog/product/k/o/kodak-easyshare-c530-5mp-digital-camera-1.jpg"
    ["types"]=>
    array(0) {
    }
  }
  [2]=>
  object(stdClass)#4 (6) {
    ["file"]=>
    string(50) "/k/o/kodak-easyshare-c530-5mp-digital-camera-2.jpg"
    ["label"]=>
    string(0) ""
    ["position"]=>
    string(1) "0"
    ["exclude"]=>
    string(1) "1"
    ["url"]=>
    string(98) "http://magento.vfeelit.com/media/catalog/product/k/o/kodak-easyshare-c530-5mp-digital-camera-2.jpg"
    ["types"]=>
    array(3) {
      [0]=>
      string(9) "thumbnail"
      [1]=>
      string(11) "small_image"
      [2]=>
      string(5) "image"
      [3]=>
      string(14) "my_media_image"
    }
  }
}

注意观察这个输出,第一第二个types是一个空数组,第三个types分别列出了4个属性(其中三个系统默认,一个自定义),它表示当前这个图片的链接保存到对应的这四个属性中(产品主图首先应用这些属性的信息,也可以把不同属性应用到不同图片)。

6 catalogProductAttributeMediaUpdate
这个方法是更新图片信息,第三参数指定图片链接,第四参数给出新图片信息,第五参数可选,指定更新只应用到指定商店的图片信息。

$productId = 1;
$file = '/i/m/image.jpg';

$newFile = array(
'content' => '',
'mime' => 'image/jpeg'
);

$result = $client->catalogProductAttributeMediaUpdate(
$session,
$productId,
$file,
array('file' => $newFile, 'label' => 'New label', 'position' => '50', 'types' => array('image'), 'exclude' => 1)
);

更新已经存在的图片的相关信息比较有用,但是如果指定了file(相当上传图片),那么跟删除了原来的图片然后新建没有什么不一样(只是更新了数据表的字段信息)。

7 catalogProductAttributeMediaCreate
这个是比较有用的方法。它可以实现在后台图片管理那里上传图片然后保存类似的操作。看例子:

$productId = 2;
$file = array(
	'content' => '',
	'mime' => 'image/jpeg'
);

$result = $proxy->catalogProductAttributeMediaCreate(
	$session,
	$productId,
	array('file' => $file, 'label' => 'Label', 'position' => '100', 'types' => array('thumbnail'), 'exclude' => 0)
);

方法的第三参数是一个数组指定了图片的相关信息,数组下标是file的索引它指向一个表示图片的数组,它又由三部分组成,content是图片经过base_64编码后的字符串(这个就相当上传图片了),第二个是mime类型,比如image/jpeg,它用来表示图片的类型,第三个可选,指定图片名字(不指定系统自命名,一般还是指定比较好,这样可以使用自己的名字)。然后label就是图片的alt信息,position是图片排序码,exclude表示是否应用到当前店铺(默认店铺是0,当然需要指定为0,不过如果当前店铺有指定,而且不希望这个图片应用到这个店铺,那么这个exclude就可以设置为1),还有就是types,它指定这个图片是否保存到类型指定的属性中,比如这里的thumbnail,那就会用新图链接更新当前产品的thumbnail对应的值。

以下是一个更加实用的例子:

//$client = new SoapClient('http://magento.vfeelit.com/api/soap/?wsdl');
$proxy = new SoapClient('http://magento.vfeelit.com/api/v2_soap/?wsdl');
$sessionId = $proxy->login('vfeelit', '1234567890');

$pid = 48;
$image = "http://magento.vfeelit.com/test.jpg";
$file = array(
	'content' => base64_encode(file_get_contents($image)),
	'mime' => 'image/jpeg',
	'name' => 'new-main-image' //如果没有指定名字,系统自动命名为image,注意:千万不要加后缀,系统将根据提供的mime自定添加后缀
);

$result = $proxy->catalogProductAttributeMediaCreate(
	$sessionId,
	$pid,
	array('file' => $file, 'label' => 'Label', 'position' => '100', 'types' => array(), 'exclude' => 0)
);
echo $result;

可以把这段代码和现有的导入程序进行整合,批量导入产品图片。

永久链接:http://blog.ifeeline.com/601.html
原创文章,转载务必保留出处。

Magento API – 批量修改产品URL

System->Configuration->Catalog->Search Engine Optimizations
Magento搜索引擎优化

Product URL Suffix					产品页面URL后缀
Category URL Suffix					目录页面URL后缀
Use Categories Path for Product URLs		是否为URL添加目录路径
Create Permanent Redirect for URLs if URL Key Changed	当URL改变时是否添加旧链接到新链接的永久定位

关于是否重定向到新链接这个开关,当编辑一个产品时:
Magento编辑产品

如果Create Permanent Redirect for URLs if URL Key Changed设置为NO时,这里的这个选择框默认就不会自动选上,当然了,编辑了这个Key之后,还是可以手动勾上的。

这里出现的URL Key,实际上对应产品实体(编辑目录时是目录实体)的url_key属性(字段),它会和Product URL Suffix组合形成一个值保存到url_path属性(字段),以上面的例子为例,nokia-2610-phone保存到url_key中,而url_path保存的就是nokia-2610-phone.html。

产品或目录的页面重写后的URL实际上最终要写入core_url_rewrite(一张索引表),当后台更新Catalog URL Rewrites,这个表将重新构建(根据元数据)。

Magento重写索引刷新

构建的依据就是上面提到的参数。看下这个表结构:

Magento core_rewite表

这里为每个商店都产生了一份URL(有两个形式):

http://learn.magento.com/electronics/cell-phones/nokia-2610-phone.html
http://learn.magento.com/nokia-2610-phone.html

这里有一个是添加了目录的URL,一个是没有添加目录前导的URL,两个都可以访问。当设置成不添加目录前导符时,只会有一个。虽然两个都能访问,但是系统实际只会暴露其中一个。

注意:虽然这里生成了很多份URL,看起来是可以为每个商店对同一个商品指定不同的URL,可是事实并非如此,后台提供的URL KEY的作用域是全局的。每次重新生成URL时都是基于url_key作为基准,纠正对应的url_path(可以改,但是会被纠正),然后使用url_path产生URL。

具体,每个实体,都只有一个url_key,它的对应store_id为0,但是有多个url_path对应不同的store_id(0,1,2,3…),它是根据url_key生成的。
Magento重写URL内容

所以,我们根本不需要去修改url_path,只需要修改url_key即可(后台刷新索引后修改的url_path就会被url_key纠正)。

下面看看在选择了Create Permanent Redirect for old URL时写入了什么内容:

Magento重写重定向

这些个东西怎么设计的就搞不懂了。

实际上,URL主要有两个元数据,url_key和url_path,这两个数据更随着产品或目录,所以我们要修改数据时,实际只要修改这两个元数据(只要改url_key)。下面通过URL来修改:

try{
	$client = new SoapClient('http://magento.vfeelit.com/api/v2_soap/?wsdl');
    $session = $client->login('vfeelit', '1234567890');

	$allProducts = $client->catalogProductList($session);
	
	foreach($allProducts as $product){

		$pid = (int)$product->product_id;
		//$pid = 16;
		// storeView 作为第三参数,最好指定,否则就是默认商店
		$entity = $client->catalogProductInfo($session,$pid);
		
		$pname = $entity->name;		
		$sku = $entity->sku;		
		
		$url_key = strtolower($pname).'-'.strtolower($sku);
				
$client->catalogProductUpdate($session,$pid,array('url_key'=>$url_key));
	}

}catch(SoapFault $e){
	echo $e->getMessage();
}catch(Exception $e){
	echo $e->getMessage();	
}

代码不多,但是相当耗费资源,你可以会得到504。为何如此耗费资源,首先获取所有产品列表,这个不算耗费资源,接下来是获取产品信息,这个是涉及多个表查询,会耗费大量资源(这个过程循环所有产品数次),然后更新属性值。所以稍微做些调整:

$entity = $client->catalogProductInfo($session,$pid);
改为:
$entity = $client->catalogProductInfo($session,$pid,'0',array('name','sku'));

这样资源的消耗降低得非常明显。检索EAV模型的数据时,一定要指定需要的属性,否则吃不消。

通过以上分析,URL的修改,其实只要修改url_key即可,其它的,在生成索引时会自动完成(纠正)。那么直接操作SQL一样可以。不过看起来和上面的例子也差不多了。

接下来看Catalog->URL Rewrite Management
Magento重写URL管理

这个就是core_url_rewrite的内容,可以编辑修改URL,不过比较悲催的是,当修改了URL之后,如果重新刷新重写索引,被修改的链接被重定向到未修改之前的链接。除非你永不刷新重写索引,否则最好不要使用这个功能。(本质上,这里是希望针对同一个产品对不同商店应用不同链接,在这里修改事实上也可以实现,就是刷新索引后数据丢失,所以比较脑残)

永久链接:http://blog.ifeeline.com/568.html
原创文章,转载务必保留出处。

Magento API 设置 Tier Price

Magento Tier Price表

先看看customer_group表:
Magento的customer_group表
customer_group 把客户分成了几类,方便管理。注意customer_group_id为0的记录,没有登录的浏览者,都归这个组。

接下来看看catalog_product_entity_tier_price表:
Magento的catalog_product_entity_tier_price表
在创建客户时有一个下拉菜单为Customer Group,它并不能选择customer_group_id为0的组(因为0表示没有登录的所有客户)。 entity_id就是对产品实体的引用,后面的qty对应了value(价格)。当创建一个产品时:
Magento产品阶梯价格设置

这里指定了website_id,数量 和 价格,如果选择了ALL GROUPS,那么all_groups就是1,customer_group_id就是0,否则all_groups为0,customer_group_id对应的组ID(此时不应该为0),明白这个逻辑非常重要

注意,上面的Website下拉块只有All Websites可选,看起来website_id总是0。如果要批量更新或插入,直接操作数据表也是可以的。不过这里通过API来练手一下。

查看http://www.magentocommerce.com/api/soap/catalog/catalogProductTierPrice/catalogProductTierPrice.html文档,发现没有v2版本的方法提供,只能使用v1版本:

try{
$client = new SoapClient('http://magento.vfeelit.com/api/soap/?wsdl');
$session = $client->login('vfeelit', '1234567890');
$tierPrices = $soap->call($session,'product_tier_price.info', ‘oc’);

print_r($tierPrices);
}

// 输出
Array
(
    [0] => Array
        (
            [customer_group_id] => all
            [website] => all
            [qty] => 5.0000
            [price] => 90.0000
        )

    [1] => Array
        (
            [customer_group_id] => all
            [website] => all
            [qty] => 10.0000
            [price] => 80.0000
        )

    [2] => Array
        (
            [customer_group_id] => all
            [website] => all
            [qty] => 20.0000
            [price] => 70.0000
        )

)

这个组织形式就是如此。customer_group_id不是all就是具体的组id(但是绝对不能指定0)。下面我们做一个批量插入的程序:

try{
	$client = new SoapClient('http://magento.vfeelit.com/api/v2_soap/?wsdl');
    $session = $client->login('vfeelit', '1234567890');

	$allProducts = $client->catalogProductList($session);
	
	foreach($allProducts as $product){
		$pid = (int)$product->product_id;
		
		$entity = $client->catalogProductInfo($session,$pid);
		$price = $entity->price;
		if((float)$price <= 10){  // 价格满足条件是才添加tier price
			continue;
		}

		// 添加阶梯,如果是针对所有组,必须指定为all,一定不要指定为0,否则出现怪异现象
		$tierPrices = array(
			array('customer_group_id' => 'all', 'website' => '0', 'qty' => '10', 'price' => round($price * 0.95, 4)),
			array('customer_group_id' => 'all', 'website' => '0', 'qty' => '20', 'price' => round($price * 0.9, 4)),
		);	
		$result = $client->catalogProductAttributeTierPriceUpdate(
			$session,
			$pid,
			$tierPrices
		);
		echo $result.'<br />';
	}

}catch(SoapFault $e){
	echo $e->getMessage();
}catch(Exception $e){
	echo $e->getMessage();	
}

这段代码执行是非常耗费资源的,所以PHP必须开足马力,比如加大超时时间,最大执行时间。

永久链接:http://blog.ifeeline.com/562.html
原创文章,转载务必保留出处。

Magento 1.7.x 获取获取全部API

$client = new SoapClient('http://magento.vfeelit.com/api/v2_soap/?wsdl');
	
$session = $client->login('vfeelit', '1234567890');
$result = $client->resources($session);
print_r($result);

数组打印:

Array
(
    [0] => stdClass Object
        (
            [title] => Store API
            [name] => core_store
            [aliases] => Array
                (
                    [0] => store
                )

            [methods] => Array
                (
                    [0] => stdClass Object
                        (
                            [title] => Retrieve store list
                            [path] => core_store.list
                            [name] => list
                            [aliases] => Array
                                (
                                    [0] => store.list
                                )

                        )

                    [1] => stdClass Object
                        (
                            [title] => Retrieve store data
                            [path] => core_store.info
                            [name] => info
                            [aliases] => Array
                                (
                                    [0] => store.info
                                )

                        )

                )

        )

    [1] => stdClass Object

输出的数组元素都是一个stdClass。看看和V1的差别:

$client = new SoapClient('http://magento.vfeelit.com/api/soap/?wsdl');
	
$session = $client->login('vfeelit', '1234567890');
$result = $client->resources($session);
print_r($result);

数组打印:

Array
(
    [0] => Array
        (
            [title] => Store API
            [description] => 
            [name] => core_store
            [aliases] => Array
                (
                    [0] => store
                )

            [methods] => Array
                (
                    [0] => Array
                        (
                            [title] => Retrieve store list
                            [description] => 
                            [path] => core_store.list
                            [name] => list
                            [aliases] => Array
                                (
                                    [0] => store.list
                                )

                        )

                    [1] => Array
                        (
                            [title] => Retrieve store data
                            [description] => 
                            [path] => core_store.info
                            [name] => info
                            [aliases] => Array
                                (
                                    [0] => store.info
                                )

                        )

                )

        )

    [1] => Array

v1 API输出全部使用数组组织,v2 API是使用stdClass包装,说明版本1 和 2不兼容。

永久链接:http://blog.ifeeline.com/560.html
原创文章,转载务必保留出处。