started shipping module
This commit is contained in:
@@ -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:
|
||||
|
@@ -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
|
@@ -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
|
16
shipping/database/Dockerfile
Normal file
16
shipping/database/Dockerfile
Normal 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
19
shipping/database/config.sh
Executable 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
27
shipping/database/convert.sh
Executable 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
|
||||
|
16
shipping/database/scripts/00-init-schema.sql
Normal file
16
shipping/database/scripts/00-init-schema.sql
Normal 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);
|
||||
|
BIN
shipping/database/scripts/10-data.sql.gz
Normal file
BIN
shipping/database/scripts/10-data.sql.gz
Normal file
Binary file not shown.
6
shipping/database/scripts/99-finished.sh
Executable file
6
shipping/database/scripts/99-finished.sh
Executable file
@@ -0,0 +1,6 @@
|
||||
#!/bin/sh
|
||||
|
||||
# signal that the import has finsihed
|
||||
sleep 5
|
||||
touch /tmp/finished
|
||||
|
@@ -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>
|
||||
|
170
shipping/service/src/main/java/org/steveww/spark/Main.java
Normal file
170
shipping/service/src/main/java/org/steveww/spark/Main.java
Normal 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();
|
||||
}
|
||||
}
|
@@ -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
|
@@ -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.
@@ -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");
|
||||
}
|
||||
}
|
@@ -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;
|
||||
|
@@ -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">€{{ item.subtotal }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="3"> </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td> </td>
|
||||
<td>Tax</td>
|
||||
<td class="currency">€{{ data.cart.tax }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td> </td>
|
||||
<td>Total</td>
|
||||
<td class="currency">€{{ data.cart.total }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="3">
|
||||
<button ng-click="buy();">Buy</button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -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;
|
||||
}
|
||||
|
||||
|
@@ -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">
|
||||
€{{ 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 + '" />'));
|
||||
|
@@ -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
13
web/static/shipping.html
Normal 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>
|
||||
|
Reference in New Issue
Block a user