PAYATHON 2026

如何在 Angular 支付流程中管理 Premium Account?

支付老李

结论

无需为 premium 用户复制一套项目创建路由,也不应把 Auth.isPremium() 作为免支付的最终判断依据。

建议采用以下设计:

  • 所有用户共用项目创建流程和路由。
  • 前端统一封装 premium 权益判断,只用于调整页面展示和交互。
  • 后端重新校验用户是否拥有有效的 premium 权益,并决定价格是否为 0。
  • 只有应付金额大于 0 时才进入支付流程。
  • 项目创建、价格确认和支付状态均由后端统一控制。

前端权限检查可以改善用户体验,但不能充当安全边界。浏览器中的 JavaScript、路由参数和用户对象都可能被修改。

将角色和付费权益分开

artist 通常是权限角色,表示用户可以执行哪些操作。premium 更接近订阅权益,可能涉及有效期、暂停、退款和套餐等级等状态。项目规模较小时,可以暂时使用角色数组:

user.roles = ['artist', 'premium'];

业务变复杂后,最好将角色和订阅分开存储:

user.roles = ['artist'];

user.subscription = {
  plan: 'premium',
  status: 'active',
  expiresAt: '2026-12-31T23:59:59Z'
};

后端判断 premium 权益时,不能只检查某个字符串是否存在,还要检查订阅状态和有效期:

function hasPremiumAccess(user, now = new Date()) {
  const subscription = user.subscription;

  return Boolean(
    subscription &&
    subscription.plan === 'premium' &&
    subscription.status === 'active' &&
    new Date(subscription.expiresAt) > now
  );
}

如果 premium 永久有效,不涉及订阅生命周期,也可以继续使用角色或权限数组。无论采用哪种数据结构,最终判断都必须在服务端完成。

在前端统一判断权益

在 AngularJS 中,可以把权益判断放进认证或权限服务,避免控制器和模板直接检查角色数组。

app.factory('EntitlementService', function (Auth) {
  function hasRole(role) {
    var user = Auth.getCurrentUser();

    return Boolean(
      user &&
      Array.isArray(user.roles) &&
      user.roles.indexOf(role) !== -1
    );
  }

  return {
    isPremium: function () {
      return hasRole('premium');
    }
  };
});

控制器根据判断结果调整页面展示:

app.controller('ProjectFormCtrl', function (
  $scope,
  EntitlementService,
  ProjectService
) {
  $scope.isPremium = EntitlementService.isPremium();

  $scope.submit = function (formData) {
    return ProjectService.create(formData);
  };
});

模板可以展示预计的处理方式:

<p ng-if="isPremium">Premium 用户创建项目免费</p>
<p ng-if="!isPremium">提交后将根据服务端返回的金额进入支付流程</p>

这里的 isPremium 只用于界面提示。即使它返回 true,前端也不能据此认定订单金额一定是 0。

无需单独建立 premium 路由

如果只有 premium 用户才能进入项目创建页,可以使用下面这样的路由配置:

$routeProvider.when('/project/:type', {
  templateUrl: 'app/project/form/form.html',
  controller: 'ProjectFormCtrl',
  access: {
    requiresLogin: true,
    requiredPermissions: ['premium']
  }
});

当前需求并不是禁止普通用户创建项目,而是让两类用户采用不同的计价规则:

  • 普通用户创建项目需要付费。
  • premium 用户创建项目免费。

两类用户执行的是同一种业务操作,因此更适合共用一个路由:

$routeProvider.when('/project/:type', {
  templateUrl: 'app/project/form/form.html',
  controller: 'ProjectFormCtrl',
  access: {
    requiresLogin: true,
    requiredPermissions: ['artist']
  }
});

如果所有已登录用户都可以创建项目,则只需保留 requiresLogin:

$routeProvider.when('/project/:type', {
  templateUrl: 'app/project/form/form.html',
  controller: 'ProjectFormCtrl',
  access: {
    requiresLogin: true
  }
});

单独设置 premium 路由会造成表单、校验和控制器逻辑重复,也不会更安全,因为前端路由守卫可以被绕过。

由后端计算价格并决定是否支付

整个流程可以这样处理:

  1. 用户提交项目数据。
  2. 后端通过登录会话或访问令牌识别用户。
  3. 后端查询用户最新的角色或订阅状态。
  4. 后端计算项目价格。
  5. premium 用户的金额为 0,项目可直接创建或激活。
  6. 普通用户获得待支付订单,随后进入支付页面。
  7. 支付完成后,后端通过支付平台回调确认订单并激活项目。

示意接口:

POST /api/projects
Authorization: Bearer <token>
Content-Type: application/json
{
  "type": "music",
  "name": "New Project"
}

后端伪代码:

async function createProject(req, res) {
  const user = await userRepository.findById(req.auth.userId);

  if (!user) {
    return res.status(401).json({
      error: 'UNAUTHENTICATED'
    });
  }

  const projectInput = validateProjectInput(req.body);
  const premium = hasPremiumAccess(user);
  const amount = premium
    ? 0
    : await pricingService.calculateProjectPrice(projectInput);

  if (amount === 0) {
    const project = await projectService.create({
      ownerId: user.id,
      input: projectInput,
      status: 'active',
      pricingReason: 'premium'
    });

    return res.status(201).json({
      project: project,
      paymentRequired: false,
      amount: 0
    });
  }

  const pendingProject = await projectService.create({
    ownerId: user.id,
    input: projectInput,
    status: 'pending_payment'
  });

  const order = await orderService.create({
    userId: user.id,
    projectId: pendingProject.id,
    amount: amount,
    currency: 'CNY'
  });

  return res.status(201).json({
    project: pendingProject,
    paymentRequired: true,
    orderId: order.id,
    amount: order.amount,
    currency: order.currency
  });
}

AngularJS 前端只需根据服务端结果选择下一步:

app.factory('ProjectService', function ($http, $location) {
  return {
    create: function (formData) {
      return $http.post('/api/projects', formData)
        .then(function (response) {
          var result = response.data;

          if (result.paymentRequired) {
            $location.path('/payment/' + result.orderId);
          } else {
            $location.path('/project/' + result.project.id);
          }

          return result;
        });
    }
  };
});

不要让客户端提交以下字段后直接采信:

{
  "isPremium": true,
  "price": 0
}

攻击者可以自行构造请求。客户端最多提交项目内容、优惠码等业务输入,最终金额必须由后端计算。

路由守卫仍有作用

路由守卫可以阻止未登录用户进入页面,也可以隐藏用户明确无权使用的功能。它负责导航控制和用户体验,不能替代后端授权。

例如,可以统一监听路由切换:

app.run(function ($rootScope, $location, Auth) {
  $rootScope.$on('$routeChangeStart', function (event, next) {
    var access = next.access || {};

    if (access.requiresLogin && !Auth.isAuthenticated()) {
      event.preventDefault();
      $location.path('/login');
      return;
    }

    if (
      access.requiredPermissions &&
      !Auth.hasAllPermissions(access.requiredPermissions)
    ) {
      event.preventDefault();
      $location.path('/forbidden');
    }
  });
});

每个对应的后端接口仍然需要执行身份认证和授权检查。

支付流程中的注意事项

不要依赖缓存中的旧用户信息

用户打开页面时可能还是 premium,但在提交项目前订阅已经过期。用户也可能刚完成升级,而前端缓存尚未更新。因此,后端应在创建订单时读取可信的最新状态。

记录计价结果

订单创建后,应保存当时确定的金额、币种和计价原因:

{
  "amount": 0,
  "currency": "CNY",
  "pricingReason": "premium",
  "premiumVerifiedAt": "2026-09-01T10:00:00Z"
}

这些记录便于审计,也有助于处理退款和订阅变更等问题。订单创建后是否会因订阅状态变化而重新计价,需要在业务规则中明确规定。

保证接口幂等

用户可能重复点击提交按钮,网络请求也可能自动重试。创建项目或支付订单的接口应支持幂等键,避免生成多个项目或发生重复扣款:

Idempotency-Key: 8fb37c5d-9d88-4d20-a2fb-30ecaec2ac70

不要把前端跳转视为支付成功

支付页面跳回成功地址,并不代表款项已经到账。后端应通过支付服务端回调或主动查询支付平台来确认交易状态,确认成功后再把项目从 pending_payment 更新为 active。

推荐的职责划分

Auth.isPremium() 并非不能使用,但它只应负责前端展示和导航,不能直接决定用户是否可以免单。

各部分职责可以这样划分:

  • Angular 权限服务负责页面提示、按钮状态和导航。
  • Angular 路由守卫负责阻止用户进入明显无权访问的页面。
  • 后端授权服务负责确认用户能否创建项目。
  • 后端权益服务负责确认 premium 是否有效。
  • 后端计价服务负责计算最终应付金额。
  • 支付服务负责创建订单并确认支付结果。

这种设计只需维护一套项目创建页面,同时可以防止用户通过修改前端状态绕过付款。

备注:内容仅供参考。