文章目录
- 从0到1新增 Order 服务
- The Orders Service
- 流程化生产 Orders Service
- 根据接口创建 Route Handlers
- 将 Orders 和 Tickets 关联上
- 创建 Order Model
- Enum 枚举的必要性
- 创建 Order Status Enum
- Mongoose Refs
- 创建 Ticket Model
- 创建 Order
- 查找已预定的 Tickets
- ticketSchema 新增 Document Methods
- 设置 Order 到期时间
- 创建测试环节
- 测试 Ticket 的存在
- 测试已过期的 Ticket
- 测试成功的情况
- 获得用户的 Orders
- 获得用户 Order 的测试
- 获得单个人的 Order
- 测试单个人的 Order
- 取消 Order
- 测试取消 Order
从0到1新增 Order 服务
The Orders Service
服务 | 功能 |
---|---|
auth | 所有事情都需要对 user 的 signup/signin/signout 进行依赖 |
tickets | Ticket 创建和编辑 并且知道是否能更新 |
orders | Order 创建和编辑 |
expiration | 监视要创建的订单,15 分钟后取消它们s |
payments | 处理付款,如果付款失败取消订单,如果付款成功则完成 |
流程化生产 Orders Service
创建 Orders Service 的流程如下
- 复制 ‘tickets’ service
- Install dependencies
- Build an image out of the orders service
- Create a Kubernetes deployment file
- Set up file sync options in the skaffold.yaml file
- Set up routing rules in the ingress service
apiVersion: apps/v1
kind: Deployment
metadata:
name: orders-depl
spec:
replicas: 1
selector:
matchLabels:
app: orders
template:
metadata:
labels:
app: orders
spec:
containers:
- name: orders
image: heysirius/orders
env:
- name: NATS_CLUSTER_ID
value: 'ticketing'
- name: NATS_CLIENT_ID
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: NATS_URL
value: 'http://nats-srv:4222'
- name: MONGO_URI
value: 'mongodb://orders-mongo-srv:27017/orders'
- name: JWT_KEY
valueFrom:
secretKeyRef:
name: jwt-secret
key: JWT_KEY
---
apiVersion: v1
kind: Service
metadata:
name: orders-srv
spec:
selector:
app: orders
ports:
- name: orders
protocol: TCP
port: 3000
targetPort: 3000
apiVersion: apps/v1
kind: Deployment
metadata:
name: orders-mongo-depl
spec:
replicas: 1
selector:
matchLabels:
app: orders-mongo
template:
metadata:
labels:
app: orders-mongo
spec:
containers:
- name: orders-mongo
image: mongo
---
apiVersion: v1
kind: Service
metadata:
name: orders-mongo-srv
spec:
selector:
app: orders-mongo
ports:
- name: db
protocol: TCP
port: 27017
targetPort: 27017
- image: heysirius/orders
context: orders
docker:
dockerfile: Dockerfile
sync:
manual:
- src: 'src/**/*.ts'
dest: .
- path: /api/orders/?(.*)
backend:
serviceName: orders-srv
servicePort: 3000
根据接口创建 Route Handlers
import express, { Request, Response } from 'express';
const router = express.Router();
router.get('/api/orders', async (req: Request, res: Response) => {
res.send({});
});
export { router as indexOrderRouter };
// routes/new.ts
router.post(
'/api/orders',
requireAuth,
[
body('ticketId')
.not()
.isEmpty()
.custom((input: string) => mongoose.Types.ObjectId.isValid(input))
.withMessage('TicketId must be provided')
],
validateRequest,
async (req: Request, res: Response) => {
res.send({});
}
);
将 Orders 和 Tickets 关联上
Option #2 is selected
创建 Order Model
import mongoose from 'mongoose';
interface OrderAttrs {
userId: string;
status: string;
expiresAt: Date;
ticket: TicketDoc;
}
interface OrderDoc extends mongoose.Document {
userId: string;
status: string;
expiresAt: Date;
ticket: TicketDoc;
}
interface OrderModel extends mongoose.Model<OrderDoc> {
build(attrs: OrderAttrs): OrderDoc;
}
const orderSchema = new mongoose.Schema(
{
userId: {
type: String,
required: true,
},
status: {
type: String,
required: true,
},
expiresAt: {
type: mongoose.Schema.Types.Date,
},
ticket: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Ticket',
},
},
{
toJSON: {
transform(doc, ret) {
ret.id = ret._id;
delete ret._id;
},
},
}
);
orderSchema.statics.build = (attrs: OrderAttrs) => {
return new Order(attrs);
};
const Order = mongoose.model<OrderDoc, OrderModel>('Order', orderSchema);
export { Order };
Enum 枚举的必要性
创建 Order Status Enum
export enum OrderStatus {
// When the order has been created, but the
// ticket it is trying to order has not been reserved
Created = 'created',
// The ticket the order is trying to reserve has already
// been reserved, or when the user has cancelled the order.
// The order expires before payment
Cancelled = 'cancelled',
// The order has successfully reserved the ticket
AwaitingPayment = 'awaiting:payment',
// The order has reserved the ticket and the user has
// provided payment successfully
Complete = 'complete',
}
Mongoose Refs
// To associate an existing Order and Ticket together
const ticket = await Ticket.findOne({});
const order = await Order.findOne({});
order.ticket = ticket;
await order.save();
// To associate an existing Ticket with a *new* Order
const ticket = await Ticket.findOne({});
const order = Order.build({
ticket: ticket,
userId: '...',
status: OrderStatus.Created,
expiresAt: tomorrow
})
// To fetch an existing Order from the database
// with its associated Ticket
const order = await Order.findbyId('...').populate('ticket');
order.ticket.title
order.ticket.price
创建 Ticket Model
import mongoose from 'mongoose';
interface TicketAttrs {
title: string;
price: number;
}
export interface TicketDoc extends mongoose.Document {
title: string;
price: number;
}
interface TicketModel extends mongoose.Model<TicketDoc> {
build(attrs: TicketAttrs): TicketDoc;
}
const ticketSchema = new mongoose.Schema(
{
title: {
type: String,
required: true,
},
price: {
type: Number,
required: true,
min: 0,
},
},
{
toJSON: {
transform(doc, ret) {
ret.id = ret._id;
delete ret._id;
},
},
}
);
ticketSchema.statics.build = (attrs: TicketAttrs) => {
return new Ticket(attrs);
};
const Ticket = mongoose.model<TicketDoc, TicketModel>('Ticket', ticketSchema);
export { Ticket };
创建 Order
// /routes/new.ts
async (req: Request, res: Response) => {
const { ticketId } = req.body;
// Find the ticket the user is trying to order in the database
const ticket = await Ticket.findById(ticketId);
if (!ticket) {
throw new NotFoundError();
}
// Make sure that this ticket is not already reserved
// Calculate an expiration date for this order
// Build the order and save it to the database
// Publish an event saying that an order was created
res.send({});
}
查找已预定的 Tickets
// /routes/new.ts
// Make sure that this ticket is not already reserved
// Run query to look at all orders. Find an order where the ticket
// is the ticket we just found *and* the orders status is *not* cancelled.
// If we find an order from that means the ticket *is* reserved
const existingOrder = await Order.findOne({
ticket: ticket,
status: {
$in: [
OrderStatus.Created,
OrderStatus.AwaitingPayment,
OrderStatus.Complete,
],
},
});
if (existingOrder) {
throw new BadRequestError('Ticket is already reserved');
}
ticketSchema 新增 Document Methods
- 这里做了个优化
- 本来是在 new.ts 里实现的 查找 已预订的 Ticket 操作
- 想了想,其实可以在 Ticket schema 里面新增一个方法来查找
- 这样 ticket 也是 this
// /models/ticket.ts
ticketSchema.methods.isReserved = async function() {
// Run query to look at all orders. Find an order where the ticket
// is the ticket we just found *and* the orders status is *not* cancelled.
// If we find an order from that means the ticket *is* reserved
const existingOrder = await Order.findOne({
ticket: this,
status: {
$in: [
OrderStatus.Created,
OrderStatus.AwaitingPayment,
OrderStatus.Complete,
],
},
});
return !!existingOrder;
}
设置 Order 到期时间
// Find the ticket the user is trying to order in the database
const ticket = await Ticket.findById(ticketId);
if (!ticket) {
throw new NotFoundError();
}
// Make sure that this ticket is not already reserved
const isReserved = await ticket.isReserved();
if (isReserved) {
throw new BadRequestError('Ticket is already reserved');
}
// Calculate an expiration date for this order
const expiration = new Date();
expiration.setSeconds(expiration.getSeconds() + EXPIRATION_WINDOW_SECONDS);
// Build the order and save it to the database
const order = Order.build({
userId: req.currentUser!.id,
status: OrderStatus.Created,
expiresAt: expiration,
ticket
});
await order.save()
// Publish an event saying that an order was created
创建测试环节
将会重用 ticket 服务的以下文件
- setup.ts
- nats-wrapper.ts
测试 Ticket 的存在
it('returns an error if the ticket does not exist', async () => {
const ticketId = mongoose.Types.ObjectId();
await request(app)
.post('/api/orders')
.set('Cookie', global.signin())
.send({ ticketId })
.expect(404);
});
测试已过期的 Ticket
it('returns an error if the ticket is already reserved', async () => {
const ticket = Ticket.build({
title: 'concert',
price: 20,
});
await ticket.save();
const order = Order.build({
ticket,
userId: 'laskdflkajsdf',
status: OrderStatus.Created,
expiresAt: new Date(),
});
await order.save();
await request(app)
.post('/api/orders')
.set('Cookie', global.signin())
.send({ ticketId: ticket.id })
.expect(400);
});
测试成功的情况
it('reserves a ticket', async () => {
const ticket = Ticket.build({
title: 'concert',
price: 20,
});
await ticket.save();
await request(app)
.post('/api/orders')
.set('Cookie', global.signin())
.send({ ticketId: ticket.id })
.expect(201);
});
获得用户的 Orders
// /routes/index.ts
import express, { Request, Response } from 'express';
import { requireAuth } from '@heysirius-common/common';
import { Order } from '../models/order';
const router = express.Router();
router.get('/api/orders', requireAuth, async (req: Request, res: Response) => {
const orders = await Order.find({
userId: req.currentUser!.id,
}).populate('ticket');
res.send(orders);
});
export { router as indexOrderRouter };
获得用户 Order 的测试
import request from 'supertest';
import { app } from '../../app';
import { Order } from '../../models/order';
import { Ticket } from '../../models/ticket';
const buildTicket = async () => {
const ticket = Ticket.build({
title: 'concert',
price: 20,
});
await ticket.save();
return ticket;
};
it('fetches orders for an particular user', async () => {
// Create three tickets
const ticketOne = await buildTicket();
const ticketTwo = await buildTicket();
const ticketThree = await buildTicket();
const userOne = global.signin();
const userTwo = global.signin();
// Create one order as User #1
await request(app)
.post('/api/orders')
.set('Cookie', userOne)
.send({ ticketId: ticketOne.id })
.expect(201);
// Create two orders as User #2
const { body: orderOne } = await request(app)
.post('/api/orders')
.set('Cookie', userTwo)
.send({ ticketId: ticketTwo.id })
.expect(201);
const { body: orderTwo } = await request(app)
.post('/api/orders')
.set('Cookie', userTwo)
.send({ ticketId: ticketThree.id })
.expect(201);
// Make request to get orders for User #2
const response = await request(app)
.get('/api/orders')
.set('Cookie', userTwo)
.expect(200);
// Make sure we only got the orders for User #2
expect(response.body.length).toEqual(2);
expect(response.body[0].id).toEqual(orderOne.id);
expect(response.body[1].id).toEqual(orderTwo.id);
expect(response.body[0].ticket.id).toEqual(ticketTwo.id);
expect(response.body[1].ticket.id).toEqual(ticketThree.id);
});
获得单个人的 Order
// /routes/show.ts
import express, { Request, Response } from 'express';
import {
requireAuth,
NotFoundError,
NotAuthorizedError,
} from '@heysirius-common/common';
import { Order } from '../models/order';
const router = express.Router();
router.get(
'/api/orders/:orderId',
requireAuth,
async (req: Request, res: Response) => {
const order = await Order.findById(req.params.orderId).populate('ticket');
if (!order) {
throw new NotFoundError();
}
if (order.userId !== req.currentUser!.id) {
throw new NotAuthorizedError();
}
res.send(order);
}
);
export { router as showOrderRouter };
测试单个人的 Order
import request from 'supertest';
import { app } from '../../app';
import { Ticket } from '../../models/ticket';
it('fetches the order', async () => {
// Create a ticket
const ticket = Ticket.build({
title: 'concert',
price: 20,
});
await ticket.save();
const user = global.signin();
// make a request to build an order with this ticket
const { body: order } = await request(app)
.post('/api/orders')
.set('Cookie', user)
.send({ ticketId: ticket.id })
.expect(201);
// make request to fetch the order
const { body: fetchedOrder } = await request(app)
.get(`/api/orders/${order.id}`)
.set('Cookie', user)
.send()
.expect(200);
expect(fetchedOrder.id).toEqual(order.id);
});
it('returns an error if one user tries to fetch another users order', async () => {
// Create a ticket
const ticket = Ticket.build({
title: 'concert',
price: 20,
});
await ticket.save();
const user = global.signin();
// make a request to build an order with this ticket
const { body: order } = await request(app)
.post('/api/orders')
.set('Cookie', user)
.send({ ticketId: ticket.id })
.expect(201);
// make request to fetch the order
await request(app)
.get(`/api/orders/${order.id}`)
.set('Cookie', global.signin())
.send()
.expect(401);
});
取消 Order
import express, { Request, Response } from 'express';
import {
requireAuth,
NotFoundError,
NotAuthorizedError,
} from '@heysirius-common/common';
import { Order, OrderStatus } from '../models/order';
const router = express.Router();
router.delete(
'/api/orders/:orderId',
requireAuth,
async (req: Request, res: Response) => {
const { orderId } = req.params;
const order = await Order.findById(orderId);
if (!order) {
throw new NotFoundError();
}
if (order.userId !== req.currentUser!.id) {
throw new NotAuthorizedError();
}
order.status = OrderStatus.Cancelled;
await order.save();
res.status(204).send(order);
}
);
export { router as deleteOrderRouter };
测试取消 Order
import request from 'supertest';
import { app } from '../../app';
import { Ticket } from '../../models/ticket';
import { Order, OrderStatus } from '../../models/order';
it('marks an order as cancelled', async () => {
// create a ticket with Ticket Model
const ticket = Ticket.build({
title: 'concert',
price: 20,
});
await ticket.save();
const user = global.signin();
// make a request to create an order
const { body: order } = await request(app)
.post('/api/orders')
.set('Cookie', user)
.send({ ticketId: ticket.id })
.expect(201);
// make a request to cancel the order
await request(app)
.delete(`/api/orders/${order.id}`)
.set('Cookie', user)
.send()
.expect(204);
// expectation to make sure the thing is cancelled
const updatedOrder = await Order.findById(order.id);
expect(updatedOrder!.status).toEqual(OrderStatus.Cancelled);
});
it.todo('emits a order cancelled event');