started shipping module

This commit is contained in:
Steve Waterworth
2018-01-12 17:26:25 +00:00
parent 7bf15b54e9
commit d294975612
23 changed files with 416 additions and 98 deletions

View File

@@ -45,10 +45,20 @@ services:
- "8080"
networks:
- robot-shop
mysql:
build:
context: shipping/database
image: steveww/rs-shipping-db
ports:
- "3306"
networks:
- robot-shop
shipping:
build:
context: shipping
context: shipping/service
image: steveww/rs-shipping
depends_on:
- mysql
ports:
- "8080"
networks:

View File

@@ -1,22 +0,0 @@
apiVersion: apps/v1beta2
kind: Deployment
metadata:
name: mongodb
namespace: robot-shop
labels:
app: mongodb
spec:
replicas: 1
selector:
matchLabels:
app: mongodb
template:
metadata:
labels:
app: mongodb
spec:
containers:
- name: mongodb
image: steveww/rs-mongodb
ports:
- containerPort: 27017

View File

@@ -1,14 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: mongodb
namespace: robot-shop
labels:
app: mongodb
spec:
ports:
- name: mongodb
port: 27017
targetPort: 27017
selector:
app: mongodb

View File

@@ -0,0 +1,16 @@
FROM mysql:5.7.20
ENV MYSQL_ALLOW_EMPTY_PASSWORD=yes \
MYSQL_DATABASE=cities \
MYSQL_USER=shipping \
MYSQL_PASSWORD=secret
# change datadir entry in /etc/mysql/my.cnf
COPY config.sh /root/
RUN /root/config.sh
COPY scripts/* /docker-entrypoint-initdb.d/
RUN /entrypoint.sh mysqld & while [ ! -f /tmp/finished ]; do sleep 10; done && mysqladmin shutdown
RUN rm /docker-entrypoint-initdb.d/*

19
shipping/database/config.sh Executable file
View File

@@ -0,0 +1,19 @@
#!/bin/sh
DIR="/etc/mysql"
FILE=$(fgrep -Rl datadir "$DIR")
if [ -n "$FILE" ]
then
# mkdir /data/mysql
echo " "
echo "Updating $FILE"
echo " "
sed -i -e '/^datadir/s/\/var\/lib\//\/data\//' $FILE
fgrep -R datadir "$DIR"
else
echo " "
echo "file not found"
echo " "
fi

27
shipping/database/convert.sh Executable file
View File

@@ -0,0 +1,27 @@
#!/bin/sh
# Convert cities CSV file to SQL
if [ -z "$1" ]
then
echo "File required as first arg"
exit 1
fi
# \x27 is a single quote
# \x60 is back tick
awk '
BEGIN {
FS=","
format = "INSERT INTO cities(country_code, city, name, region, latitude, longitude) VALUES(\x27%s\x27, \x27%s\x27, \x27%s\x27, \x27%s\x27, %s, %s);\n"
getline
}
{
gsub(/\x27/, "\x60", $2)
gsub(/\x27/, "\x60", $3)
gsub(/\x27/, "\x60", $4)
if(NF == 6) printf format, $1, $2, $3, $4, $5, $6
else printf format, $1, $2, $3, $4, $6, $7
}
' $1

View File

@@ -0,0 +1,16 @@
CREATE TABLE cities (
uuid int auto_increment primary key,
country_code varchar(2),
city varchar(100),
name varchar(100),
region varchar(100),
latitude decimal(10, 7),
longitude decimal(10, 7)
) ENGINE=InnoDB;
CREATE FULLTEXT INDEX city_idx ON cities(city);
CREATE INDEX region_idx ON cities(region);
CREATE INDEX c_code_idx ON cities(country_code);

Binary file not shown.

View File

@@ -0,0 +1,6 @@
#!/bin/sh
# signal that the import has finsihed
sleep 5
touch /tmp/finished

View File

@@ -23,9 +23,14 @@
<version>1.7.25</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-jmx</artifactId>
<version>9.4.6.v20170531</version>
<groupId>c3p0</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.1.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.45</version>
</dependency>
</dependencies>

View File

@@ -0,0 +1,170 @@
package org.steveww.spark;
import com.mchange.v2.c3p0.ComboPooledDataSource;
import spark.Spark;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.sql.Connection;
import java.sql.Statement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.Types;
public class Main {
private static Logger logger = LoggerFactory.getLogger(Main.class);
private static ComboPooledDataSource cpds = null;
public static void main(String[] args) {
//
// Create database connector
// TODO - might need a retry loop here
//
try {
cpds = new ComboPooledDataSource();
cpds.setDriverClass( "com.mysql.jdbc.Driver" ); //loads the jdbc driver
cpds.setJdbcUrl( "jdbc:mysql://mysql/cities?useSSL=false&autoReconnect=true" );
cpds.setUser("shipping");
cpds.setPassword("secret");
// some config
cpds.setMinPoolSize(5);
cpds.setAcquireIncrement(5);
cpds.setMaxPoolSize(20);
cpds.setMaxStatements(180);
}
catch(Exception e) {
logger.error("Database Exception", e);
}
// Spark
Spark.port(8080);
Spark.get("/health", (req, res) -> "OK");
Spark.get("/count", (req, res) -> {
String data = query("select count(*) as count from cities");
res.header("Content-Type", "application/json");
return data;
});
Spark.get("/codes", (req, res) -> {
String data = query("select distinct country_code as code from cities order by code asc");
res.header("Content-Type", "application/json");
return data;
});
Spark.get("/match/:code/:text", (req, res) -> {
String query = "select name from cities where country_code ='" + req.params(":code") + "' and city like '" + req.params(":text") + "%' order by name asc limit 10";
logger.info("Query " + query);
String data = query(query);
res.header("Content-Type", "application/json");
return data;
});
logger.info("Ready");
}
private static String query(String query) {
StringBuilder buffer = new StringBuilder();
Connection conn = null;
Statement stmt = null;
ResultSet rs = null;
try {
conn = cpds.getConnection();
stmt = conn.createStatement();
rs = stmt.executeQuery(query);
ResultSetMetaData metadata = rs.getMetaData();
int colCount = metadata.getColumnCount();
buffer.append('[');
while(rs.next()) {
// result set to JSON
buffer.append('{');
for(int idx = 1; idx <= colCount; idx++) {
String name = metadata.getColumnLabel(idx);
switch(metadata.getColumnType(idx)) {
case Types.INTEGER:
int i = rs.getInt(idx);
buffer.append(write(name, rs.getInt(idx)));
break;
case Types.BIGINT:
buffer.append(write(name, rs.getLong(idx)));
break;
case Types.DECIMAL:
case Types.NUMERIC:
buffer.append(write(name, rs.getBigDecimal(idx)));
break;
case Types.FLOAT:
case Types.REAL:
case Types.DOUBLE:
buffer.append(write(name, rs.getDouble(idx)));
break;
case Types.NVARCHAR:
case Types.VARCHAR:
case Types.LONGNVARCHAR:
case Types.LONGVARCHAR:
buffer.append(write(name, '"' + rs.getString(idx) + '"'));
break;
case Types.TINYINT:
case Types.SMALLINT:
buffer.append(write(name, rs.getShort(idx)));
break;
default:
logger.info("Unknown type " + metadata.getColumnType(idx));
}
if(idx != colCount) {
buffer.append(',');
}
}
buffer.append("}, ");
}
// trim off trailing ,
int idx = buffer.lastIndexOf(",");
if(idx != -1) {
buffer.setCharAt(idx, ' ');
}
buffer.append(']');
}
catch(Exception e) {
logger.error("Query Exception", e);
}
finally {
if(rs != null) {
try {
rs.close();
} catch(Exception e) {
logger.error("Close Exception", e);
}
}
if(stmt != null) {
try {
stmt.close();
} catch(Exception e) {
logger.error("Close Exception", e);
}
}
if(conn != null) {
try {
conn.close();
} catch(Exception e) {
logger.error("Close Exception", e);
}
}
}
return buffer.toString();
}
private static String write(String key, Object val) {
StringBuilder buffer = new StringBuilder();
buffer.append('"').append(key).append('"').append(": ").append(val);
return buffer.toString();
}
}

View File

@@ -1,22 +0,0 @@
apiVersion: apps/v1beta2
kind: Deployment
metadata:
name: shipping
namespace: robot-shop
labels:
app: shipping
spec:
replicas: 1
selector:
matchLabels:
app: shipping
template:
metadata:
labels:
app: shipping
spec:
containers:
- name: shipping
image: steveww/rs-shipping
ports:
- containerPort: 8080

View File

@@ -1,14 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: shipping
namespace: robot-shop
labels:
app: shipping
spec:
ports:
- name: shipping
port: 8080
targetPort: 8080
selector:
app: shipping

Binary file not shown.

View File

@@ -1,17 +0,0 @@
package org.steveww.spark;
import spark.Spark;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Main {
private static Logger logger = LoggerFactory.getLogger(Main.class);
public static void main(String[] args) {
Spark.port(8080);
Spark.get("/hello", (req, res) -> "Hello World");
Spark.awaitInitialization();
logger.info("Ready");
}
}

View File

@@ -54,6 +54,10 @@ server {
proxy_pass http://cart:8080/;
}
location /api/shipping/ {
proxy_pass http://shipping:8080/;
}
location /nginx_status {
stub_status on;
access_log off;

View File

@@ -1,4 +1,39 @@
<!-- shopping cart -->
<div>
Shopping cart for {{ data.uniqueid }} will be here
Shopping cart for {{ data.uniqueid }}
<div ng-if="data.cart.total == 0">
Your cart is empty, get shopping
</div>
<div ng-if="data.cart.total != 0">
<table>
<tr>
<th>QTY</th>
<th>Name</th>
<th>Sub Total</th>
</tr>
<tr ng-repeat="item in data.cart.items">
<td><input type="number" size="2" min="0" max="10" ng-model="item.qty" ng-change="change(item.sku, item.qty);"/></td>
<td>{{ item.name }}</td>
<td class="currency">&euro;{{ item.subtotal }}</td>
</tr>
<tr>
<td colspan="3">&nbsp;</td>
</tr>
<tr>
<td>&nbsp;</td>
<td>Tax</td>
<td class="currency">&euro;{{ data.cart.tax }}</td>
</tr>
<tr>
<td>&nbsp;</td>
<td>Total</td>
<td class="currency">&euro;{{ data.cart.total }}</td>
</tr>
<tr>
<td colspan="3">
<button ng-click="buy();">Buy</button>
</td>
</tr>
</table>
</div>
</div>

View File

@@ -43,6 +43,10 @@ h3 a:visited {
text-transform: capitalize;
}
.carttotal {
margin-left: 25px;
}
ul.products {
margin: 5px;
}
@@ -145,3 +149,8 @@ table.credentials {
text-align: center;
}
/* cart */
.currency {
text-align: right;
}

View File

@@ -28,6 +28,12 @@
<div class="nav column">
<h3><a href="login">Login / Register</a></h3>
<h3><a href="cart">Cart</a></h3>
<div ng-if="data.cart.total == 0" class="carttotal">
Empty
</div>
<div ng-if="data.cart.total != 0" class="carttotal">
&euro;{{ data.cart.total }}
</div>
<h3>Categories</h3>
<ul class="products">
<li ng-repeat="cat in data.categories">
@@ -54,8 +60,8 @@
</div>
<!-- JavaScript -->
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.7/angular.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.7/angular-route.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.7/angular.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.7/angular-route.js"></script>
<script src="js/controller.js"></script>
<script type="text/javascript">
angular.element(document.getElementsByTagName('head')).append(angular.element('<base href="' + window.location.pathname + '" />'));

View File

@@ -8,6 +8,9 @@
var data = {
uniqueid: '',
user: {},
cart: {
total: 0
}
};
return data;
@@ -26,6 +29,9 @@
}).when('/cart', {
templateUrl: 'cart.html',
controller: 'cartform'
}).when('/shipping', {
templateUrl: 'shipping.html',
controller: 'shipform'
});
// needed for URL rewrite hash
@@ -46,6 +52,10 @@
$scope.data.uniqueid = 'foo';
$scope.data.categories = [];
$scope.data.products = {};
// empty cart
$scope.data.cart = {
total: 0
};
$scope.getProducts = function(category) {
if($scope.data.products[category]) {
@@ -108,6 +118,13 @@
$scope.data.uniqueid = currentUser.uniqueid;
}
});
// watch for cart changes
$scope.$watch(() => { return currentUser.cart.total; }, (newVal, oldVal) => {
if(newVal !== oldVal) {
$scope.data.cart = currentUser.cart;
}
});
});
robotshop.controller('productform', function($scope, $http, $routeParams, $timeout, currentUser) {
@@ -123,7 +140,8 @@
url: url,
method: 'GET'
}).then((res) => {
console.log('cart', res);
console.log('cart', res.data);
currentUser.cart = res.data;
$scope.data.message = 'Added to cart';
$timeout(clearMessage, 3000);
}).catch((e) => {
@@ -152,9 +170,62 @@
loadProduct($routeParams.sku);
});
robotshop.controller('cartform', function($scope, $http, currentUser) {
robotshop.controller('cartform', function($scope, $http, $location, currentUser) {
$scope.data = {};
$scope.data.cart = {};
$scope.data.cart.total = 0;
$scope.data.uniqueid = currentUser.uniqueid;
$scope.buy = function() {
$location.url('/shipping');
};
$scope.change = function(sku, qty) {
// update the cart
var url = '/api/cart/update/' + $scope.data.uniqueid + '/' + sku + '/' + qty;
console.log('change', url);
$http({
url: url,
method: 'GET'
}).then((res) => {
$scope.data.cart = res.data;
currentUser.cart = res.data;
}).catch((e) => {
console.log('ERROR', e);
});
};
function loadCart(id) {
$http({
url: '/api/cart/cart/' + id,
method: 'GET'
}).then((res) => {
$scope.data.cart = res.data;
}).catch((e) => {
console.log('ERROR', e);
});
}
loadCart($scope.data.uniqueid);
});
robotshop.controller('shipform', function($scope, $http, currentUser) {
$scope.data = {};
$scope.data.codes = [ ];
function loadCodes() {
$http({
url: '/api/shipping/codes',
method: 'GET'
}).then((res) => {
$scope.data.codes = res.data;
}).catch((e) => {
console.log('ERROR', e);
});
}
loadCodes();
console.log('shipform init');
});
robotshop.controller('loginform', function($scope, $http, currentUser) {

13
web/static/shipping.html Normal file
View File

@@ -0,0 +1,13 @@
<!-- shipping template -->
<div>
<h3>Shipping information</h3>
<div>
Enter country code <select ng-model="data.selectedCode" ng-options="opt.code for opt in data.codes"></select>
</div>
<div>
Enter your location <input type="text" size="20" ng-model="data.location" ng-disabled="!data.selectedCode"/>
<button ng-click="addLocation();">Calculate</button>
</div>
<!-- TODO - add in shipping distance and cost -->
</div>