From 55d694aaae4e421b5e55cd941500e08f85cb5d4d Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期四, 27 八月 2026 18:21:33 +0800
Subject: [PATCH] feat(sales): 添加框架合同订单功能并优化销售台账管理

---
 src/main/java/com/ruoyi/sales/service/impl/SalesLedgerServiceImpl.java |  236 +++++++++++++++++++++++++++++++++++++++++++++-------------
 1 files changed, 181 insertions(+), 55 deletions(-)

diff --git a/src/main/java/com/ruoyi/sales/service/impl/SalesLedgerServiceImpl.java b/src/main/java/com/ruoyi/sales/service/impl/SalesLedgerServiceImpl.java
index fe385f1..3f223ca 100644
--- a/src/main/java/com/ruoyi/sales/service/impl/SalesLedgerServiceImpl.java
+++ b/src/main/java/com/ruoyi/sales/service/impl/SalesLedgerServiceImpl.java
@@ -1,6 +1,7 @@
 package com.ruoyi.sales.service.impl;
 
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
@@ -38,8 +39,6 @@
 import com.ruoyi.project.system.domain.SysUser;
 import com.ruoyi.project.system.mapper.SysDeptMapper;
 import com.ruoyi.project.system.mapper.SysUserMapper;
-import com.ruoyi.purchase.dto.SimpleReturnOrderGroupDto;
-import com.ruoyi.purchase.mapper.PurchaseReturnOrderProductsMapper;
 import com.ruoyi.quality.mapper.QualityInspectMapper;
 import com.ruoyi.sales.dto.*;
 import com.ruoyi.sales.mapper.CommonFileMapper;
@@ -51,6 +50,7 @@
 import com.ruoyi.sales.pojo.SalesLedgerProduct;
 import com.ruoyi.sales.pojo.ShippingInfo;
 import com.ruoyi.sales.service.ISalesLedgerService;
+import com.ruoyi.sales.vo.FrameworkOrderVo;
 import com.ruoyi.sales.vo.SalesLedgerVo;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
@@ -63,9 +63,9 @@
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.web.multipart.MultipartFile;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 
 import java.io.InputStream;
-import java.lang.reflect.Field;
 import java.math.BigDecimal;
 import java.math.RoundingMode;
 import java.time.LocalDate;
@@ -120,9 +120,6 @@
     @Autowired
     private ProductionProductMainService productionProductMainService;
     @Autowired
-    private PurchaseReturnOrderProductsMapper purchaseReturnOrderProductsMapper;
-    ;
-    @Autowired
     private SysUserMapper sysUserMapper;
 
     @Override
@@ -147,17 +144,11 @@
         productWrapper.eq(SalesLedgerProduct::getType, type.getCode());
         List<SalesLedgerProduct> salesLedgerProducts = salesLedgerProductMapper.selectList(productWrapper);
         if (type.equals(SaleEnum.PURCHASE)) {
-            // 鏌ヨ閫�璐т俊鎭�
-            List<Long> productIds = salesLedgerProducts.stream().map(SalesLedgerProduct::getProductModelId).collect(Collectors.toList());
-            List<SimpleReturnOrderGroupDto> groupListByProductIds = new ArrayList<>();
-            if(CollectionUtils.isNotEmpty(productIds)){
-                groupListByProductIds = purchaseReturnOrderProductsMapper.getReturnOrderGroupListByProductIds(productIds);
-            }
-            Map<Long, BigDecimal> returnOrderGroupDtoMap = groupListByProductIds.stream().collect(Collectors.toMap(SimpleReturnOrderGroupDto::getProductModelId, SimpleReturnOrderGroupDto::getSumReturnQuantity));
             salesLedgerProducts.forEach(item -> {
-                BigDecimal returnQuality = returnOrderGroupDtoMap.getOrDefault(item.getProductModelId(), BigDecimal.ZERO);
-                item.setReturnQuality(returnQuality);
-                item.setAvailableQuality(item.getQuantity().subtract(returnQuality));
+                BigDecimal returnQuantity = item.getReturnQuantity() == null ? BigDecimal.ZERO : item.getReturnQuantity();
+                item.setReturnQuantity(returnQuantity);
+                BigDecimal quantity = item.getQuantity() == null ? BigDecimal.ZERO : item.getQuantity();
+                item.setAvailableQuality(quantity.subtract(returnQuantity));
             });
         }
         return salesLedgerProducts;
@@ -330,9 +321,17 @@
             if (CollectionUtils.isEmpty(salesLedgerImportDtoList)) return AjaxResult.error("閿�鍞彴璐︽暟鎹负绌猴紒");
             List<SalesLedgerImportDto> salesLedgerProductImportDtoList = stringListMap.get("閿�鍞骇鍝佹暟鎹�");
             if (CollectionUtils.isEmpty(salesLedgerProductImportDtoList)) return AjaxResult.error("閿�鍞骇鍝佹暟鎹负绌猴紒");
-            // 瀹㈡埛鏁版嵁
-            List<Customer> customers = customerMapper.selectList(new LambdaQueryWrapper<Customer>().in(Customer::getCustomerName,
-                    salesLedgerImportDtoList.stream().map(SalesLedgerImportDto::getCustomerName).collect(Collectors.toList())));
+            // 瀹㈡埛鏁版嵁 - 鍙傝�� listPage 鏌ヨ绉佹捣瀹㈡埛锛坱ype = 0锛�
+            // type = 0锛堢娴峰鎴凤級鎴栬�� type = 1锛堝叕娴峰鎴凤級涓斿凡琚垎閰嶏紝骞朵笖鏄嚜宸遍鐢ㄣ�佽嚜宸卞垱寤烘垨鑰呭叡浜粰鑷繁鐨勫鎴�
+            Long loginUserId = loginUser.getUser().getUserId();
+            List<Customer> customers = customerMapper.selectList(new QueryWrapper<Customer>()
+                    .in("customer_name", salesLedgerImportDtoList.stream()
+                            .map(SalesLedgerImportDto::getCustomerName).collect(Collectors.toList()))
+                    .and(wrapper -> wrapper.eq("type", 0)
+                            .or(wrapper2 -> wrapper2.eq("type", 1).eq("is_assigned", 1)))
+                    .and(wrapper -> wrapper.eq("usage_user", loginUserId)
+                            .or(wrapper2 -> wrapper2.eq("create_user", loginUserId)
+                                    .or(wrapper3 -> wrapper3.exists("select 1 from customer_user cu where cu.customer_id = customer.id and cu.user_id = " + loginUserId)))));
 //            // 瑙勬牸鍨嬪彿鏁版嵁
 //            List<ProductModel> productModels = productModelMapper.selectList(new LambdaQueryWrapper<ProductModel>().in(ProductModel::getModel,
 //                    salesLedgerProductImportDtoList.stream().map(SalesLedgerImportDto::getSpecificationModel).collect(Collectors.toList())));
@@ -350,21 +349,26 @@
                 if (salesLedger1 != null) {
                     continue;
                 }
+
+                // 鍒ゆ柇涓氬姟鍛樻槸鍚﹀瓨鍦�
+                SysUser salesman = sysUserMapper.selectOne(new LambdaQueryWrapper<SysUser>()
+                        .eq(SysUser::getNickName, salesLedgerImportDto.getSalesman()));
+                if (salesman == null) {
+                    throw new RuntimeException("涓氬姟鍛�:" + salesLedgerImportDto.getSalesman() + "涓嶅瓨鍦紒");
+                }
                 SalesLedger salesLedger = new SalesLedger();
                 BeanUtils.copyProperties(salesLedgerImportDto, salesLedger);
                 salesLedger.setExecutionDate(DateUtils.toLocalDate(salesLedgerImportDto.getExecutionDate()));
                 salesLedger.setDeliveryDate(DateUtils.toLocalDate(salesLedgerImportDto.getDeliveryDate()));
                 // 閫氳繃瀹㈡埛鍚嶇О鏌ヨ瀹㈡埛ID锛屽鎴峰悎鍚屽彿
-                salesLedger.setCustomerId(customers.stream()
+                Optional<Customer> customerOptional = customers.stream()
                         .filter(customer -> customer.getCustomerName().equals(salesLedger.getCustomerName()))
-                        .findFirst()
-                        .map(Customer::getId)
-                        .orElse(null));
-                salesLedger.setCustomerContractNo(customers.stream()
-                        .filter(customer -> customer.getCustomerName().equals(salesLedger.getCustomerName()))
-                        .findFirst()
-                        .map(Customer::getTaxpayerIdentificationNumber)
-                        .orElse(null));
+                        .findFirst();
+                if (customerOptional.isEmpty()) {
+                    throw new RuntimeException("瀹㈡埛:" + salesLedger.getCustomerName() + "涓嶅瓨鍦紒鎴栬�呴潪绉佹捣鐢ㄦ埛");
+                }
+                salesLedger.setCustomerId(customerOptional.get().getId());
+                salesLedger.setCustomerContractNo(customerOptional.get().getTaxpayerIdentificationNumber());
                 Long aLong = sysUsers.stream()
                         .filter(sysUser -> sysUser.getNickName().equals(salesLedger.getEntryPerson()))
                         .findFirst()
@@ -382,6 +386,7 @@
                 salesLedger.setContractAmount(salesLedgerProductImportDtos.stream()
                         .map(SalesLedgerProductImportDto::getTaxInclusiveTotalPrice)
                         .reduce(BigDecimal.ZERO, BigDecimal::add));
+                salesLedger.setNetContractAmount(salesLedger.getContractAmount());
                 salesLedgerMapper.insert(salesLedger);
 
 
@@ -392,13 +397,16 @@
                     salesLedgerProduct.setType(1);
                     // 璁$畻涓嶅惈绋庢�讳环
                     salesLedgerProduct.setTaxExclusiveTotalPrice(salesLedgerProduct.getTaxInclusiveTotalPrice().divide(new BigDecimal(1).add(salesLedgerProduct.getTaxRate().divide(new BigDecimal(100))), 2, RoundingMode.HALF_UP));
-                    list.stream()
+                    // 鏍¢獙浜у搧瑙勬牸鏄惁瀛樺湪
+                    Optional<Map<String, Object>> productModelOptional = list.stream()
                             .filter(map -> Objects.equals(map.get("productName"), salesLedgerProduct.getProductCategory()) && Objects.equals(map.get("model"), salesLedgerProduct.getSpecificationModel()))
-                            .findFirst()
-                            .ifPresent(map -> {
-                                salesLedgerProduct.setProductModelId(Long.parseLong(map.get("modelId").toString()));
-                                salesLedgerProduct.setProductId(Long.parseLong(map.get("id").toString()));
-                            });
+                            .findFirst();
+                    if (productModelOptional.isEmpty()) {
+                        throw new RuntimeException("浜у搧澶х被:" + salesLedgerProduct.getProductCategory() + ",瑙勬牸鍨嬪彿:" + salesLedgerProduct.getSpecificationModel() + "涓嶅瓨鍦紒");
+                    }
+                    Map<String, Object> productModelMap = productModelOptional.get();
+                    salesLedgerProduct.setProductModelId(Long.parseLong(productModelMap.get("modelId").toString()));
+                    salesLedgerProduct.setProductId(Long.parseLong(productModelMap.get("id").toString()));
 //                    salesLedgerProduct.setProductId(productList.stream()
 //                            .filter(product -> product.getProductName().equals(salesLedgerProduct.getProductCategory()))
 //                            .findFirst()
@@ -414,8 +422,7 @@
                     salesLedgerProduct.setApproveStatus(0);
                     salesLedgerProduct.setIsProduction(salesLedgerProductImportDto.getIsProduction() == 1);
                     salesLedgerProductMapper.insert(salesLedgerProduct);
-                    // 娣诲姞鐢熶骇鏁版嵁
-                    salesLedgerProductServiceImpl.addProductionData(salesLedgerProduct);
+                    addProductionDataIfNeeded(salesLedgerProduct);
                 }
             }
 
@@ -501,6 +508,11 @@
         if (CollectionUtils.isEmpty(idList)) {
             return 0;
         }
+        long childCount = salesLedgerMapper.selectCount(new LambdaQueryWrapper<SalesLedger>()
+                .in(SalesLedger::getParentContractId, idList));
+        if (childCount > 0) {
+            throw new BaseException("妗嗘灦鍚堝悓瀛樺湪妗嗘灦璁㈠崟锛屼笉鑳藉垹闄�");
+        }
         // 鍒犻櫎閿�鍞鐞嗘暟鎹�
         LambdaQueryWrapper<SalesLedgerProduct> queryWrapper = new LambdaQueryWrapper<>();
         queryWrapper.in(SalesLedgerProduct::getSalesLedgerId, idList)
@@ -544,6 +556,10 @@
     @Override
     @Transactional(rollbackFor = Exception.class)
     public int addOrUpdateSalesLedger(SalesLedgerDto salesLedgerDto) {
+        log.info("addOrUpdateSalesLedger 鏀跺埌 contractType={}, parentContractId={}, id={}",
+                salesLedgerDto == null ? null : salesLedgerDto.getContractType(),
+                salesLedgerDto == null ? null : salesLedgerDto.getParentContractId(),
+                salesLedgerDto == null ? null : salesLedgerDto.getId());
         // 1. 鏍¢獙瀹㈡埛淇℃伅
         Customer customer = customerMapper.selectById(salesLedgerDto.getCustomerId());
         if (customer == null) {
@@ -552,19 +568,27 @@
 
         // 2. DTO杞珽ntity
         SalesLedger salesLedger = convertToEntity(salesLedgerDto);
+        log.info("convertToEntity 鍚� contractType={}", salesLedger.getContractType());
         salesLedger.setCustomerName(customer.getCustomerName());
         salesLedger.setTenantId(customer.getTenantId());
+        // 褰曞叆浜猴細濡傛灉鏈紶鍒欎娇鐢ㄥ綋鍓嶇櫥褰曠敤鎴�
+        if (StringUtils.isEmpty(salesLedger.getEntryPerson())) {
+            salesLedger.setEntryPerson(String.valueOf(SecurityUtils.getUserId()));
+        }
         // 3. 鏂板鎴栨洿鏂颁富琛�
         if (salesLedger.getId() == null) {
             String contractNo = salesLedger.getSalesContractNo();
             if (StringUtils.isEmpty(contractNo)) {
-                contractNo = generateSalesContractNo();
+                contractNo = generateSalesContractNo(salesLedgerDto.getEntryDate());
             }
             salesLedger.setSalesContractNo(contractNo);
             salesLedgerMapper.insert(salesLedger);
         } else {
             salesLedgerMapper.updateById(salesLedger);
         }
+        // 璇婃柇锛歩nsert/update 鍚庡洖鏌ユ暟鎹簱瀹為檯鍐欏叆鐨� contract_type
+        SalesLedger dbCheckLedger = salesLedgerMapper.selectById(salesLedger.getId());
+        log.info("DB 瀹為檯 contract_type={}, id={}", dbCheckLedger == null ? null : dbCheckLedger.getContractType(), salesLedger.getId());
 
 
         // 4. 澶勭悊瀛愯〃鏁版嵁
@@ -580,10 +604,101 @@
             );
         }
 
-        // 5. 淇濆瓨閿�鍞彴璐﹂檮浠�
+        // 5. 鍏滃簳锛氱‘淇濆疄闄呭悎鍚岄噾棰濇湁鍒濆鍊肩瓑浜庡悎鍚岄噾棰�
+        if (salesLedger.getNetContractAmount() == null) {
+            SalesLedger dbLedger = salesLedgerMapper.selectById(salesLedger.getId());
+            if (dbLedger != null && dbLedger.getNetContractAmount() == null && dbLedger.getContractAmount() != null) {
+                SalesLedger updateEntity = new SalesLedger();
+                updateEntity.setId(salesLedger.getId());
+                updateEntity.setNetContractAmount(dbLedger.getContractAmount());
+                salesLedgerMapper.updateById(updateEntity);
+            }
+        }
+
+        // 6. 淇濆瓨閿�鍞彴璐﹂檮浠�
         fileUtil.saveStorageAttachment(ApplicationTypeEnum.FILE, RecordTypeEnum.SALES_LEDGER, salesLedger.getId(), salesLedgerDto.getStorageBlobDTOs());
 
         return 1;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public int createFrameworkOrder(SalesLedgerDto dto) {
+        if (dto == null || dto.getId() == null) {
+            throw new BaseException("璇烽�夋嫨妗嗘灦鍚堝悓");
+        }
+        SalesLedger parent = salesLedgerMapper.selectById(dto.getId());
+        if (parent == null || !Objects.equals(parent.getContractType(), 2)) {
+            throw new BaseException("鐖跺彴璐︿笉鏄鏋跺悎鍚�");
+        }
+        if (CollectionUtils.isEmpty(dto.getProductData())) {
+            throw new BaseException("璇峰~鍐欐鏋惰鍗曚骇鍝�");
+        }
+        SalesLedger order = new SalesLedger();
+        order.setCustomerContractNo(parent.getCustomerContractNo());
+        order.setProjectName(parent.getProjectName());
+        order.setSalesman(parent.getSalesman());
+        order.setCustomerId(parent.getCustomerId());
+        order.setCustomerName(parent.getCustomerName());
+        order.setEntryPerson(String.valueOf(SecurityUtils.getUserId()));
+        order.setRemarks(dto.getRemarks());
+        order.setTenantId(parent.getTenantId());
+        order.setExecutionDate(parent.getExecutionDate());
+        order.setPaymentMethod(parent.getPaymentMethod());
+        order.setEntryDate(parent.getEntryDate());
+        order.setId(null);
+        order.setContractType(3);
+        order.setParentContractId(parent.getId());
+        order.setSalesContractNo(generateSalesContractNo(new Date()));
+        order.setDeliveryDate(null);
+        order.setContractAmount(BigDecimal.ZERO);
+        order.setNetContractAmount(BigDecimal.ZERO);
+        salesLedgerMapper.insert(order);
+
+        Set<Long> parentProductIds = dto.getProductData().stream()
+                .map(SalesLedgerProduct::getParentProductId)
+                .collect(Collectors.toSet());
+        if (parentProductIds.contains(null) || parentProductIds.size() != dto.getProductData().size()) {
+            throw new BaseException("妗嗘灦璁㈠崟浜у搧蹇呴』鍏宠仈涓嶅悓鐨勭埗浜у搧");
+        }
+        Map<Long, SalesLedgerProduct> parentProducts = salesLedgerProductMapper.selectBatchIds(parentProductIds).stream()
+                .collect(Collectors.toMap(SalesLedgerProduct::getId, Function.identity()));
+        for (SalesLedgerProduct requestProduct : dto.getProductData()) {
+            if (requestProduct.getDeliveryDate() == null) {
+                throw new BaseException("妗嗘灦璁㈠崟浜у搧蹇呴』鍏宠仈鐖朵骇鍝佸苟濉啓浜よ揣鏃ユ湡");
+            }
+            SalesLedgerProduct parentProduct = parentProducts.get(requestProduct.getParentProductId());
+            if (parentProduct == null || !Objects.equals(parentProduct.getSalesLedgerId(), parent.getId())
+                    || !Objects.equals(parentProduct.getType(), SaleEnum.SALE.getCode())) {
+                throw new BaseException("妗嗘灦璁㈠崟浜у搧涓嶅睘浜庤妗嗘灦鍚堝悓");
+            }
+            SalesLedgerProduct product = new SalesLedgerProduct();
+            BeanUtils.copyProperties(parentProduct, product);
+            product.setId(null);
+            product.setSalesLedgerId(order.getId());
+            product.setParentProductId(parentProduct.getId());
+            product.setQuantity(null);
+            product.setDeliveryDate(requestProduct.getDeliveryDate());
+            product.setIsProduction(false);
+            product.setType(SaleEnum.SALE.getCode());
+            if (product.getTaxRate() == null) {
+                product.setTaxRate(BigDecimal.ZERO);
+            }
+            salesLedgerProductMapper.insert(product);
+            // 妗嗘灦璁㈠崟鍒涘缓鏃剁敓鎴愮敓浜ц鍒掞紙鏁伴噺鐣欑┖锛屼笅鍙戞椂濉啓锛�
+            salesLedgerProductServiceImpl.addProductionData(product);
+        }
+        return 1;
+    }
+
+    @Override
+    public List<FrameworkOrderVo> listFrameworkOrders(Long parentContractId) {
+        return salesLedgerMapper.listFrameworkOrders(parentContractId);
+    }
+
+    @Override
+    public BigDecimal getFrameworkContractShippedQuantity(Long parentContractId) {
+        return salesLedgerMapper.sumShippedByParentContract(parentContractId);
     }
 
     @Override
@@ -607,10 +722,20 @@
         if (!insertList.isEmpty()) {
             for (SalesLedgerProduct salesLedgerProduct : insertList) {
                 salesLedgerProduct.setType(type.getCode());
+                if (salesLedgerProduct.getTaxRate() == null) {
+                    salesLedgerProduct.setTaxRate(BigDecimal.ZERO);
+                }
                 salesLedgerProductMapper.insert(salesLedgerProduct);
-                // 娣诲姞鐢熶骇鏁版嵁
-                salesLedgerProductServiceImpl.addProductionData(salesLedgerProduct);
+                addProductionDataIfNeeded(salesLedgerProduct);
             }
+        }
+    }
+
+    private void addProductionDataIfNeeded(SalesLedgerProduct product) {
+        SalesLedger ledger = salesLedgerMapper.selectById(product.getSalesLedgerId());
+        if (ledger != null && !Objects.equals(ledger.getContractType(), 2)
+                && !Objects.equals(ledger.getContractType(), 3)) {
+            salesLedgerProductServiceImpl.addProductionData(product);
         }
     }
 
@@ -621,8 +746,8 @@
     }
 
     @Transactional(readOnly = true)
-    public String generateSalesContractNo() {
-        LocalDate currentDate = LocalDate.now();
+    public String generateSalesContractNo(Date entryDate) {
+        LocalDate currentDate = entryDate != null ? DateUtils.toLocalDate(entryDate) : LocalDate.now();
         String datePart = currentDate.format(DateTimeFormatter.BASIC_ISO_DATE);
         String lockKey = LOCK_PREFIX + datePart;
         String lockValue = Thread.currentThread().getId() + "-" + System.nanoTime(); // 鍞竴鏍囪瘑閿佹寔鏈夎��
@@ -707,21 +832,22 @@
                 .filter(Objects::nonNull)
                 .reduce(BigDecimal.ZERO, BigDecimal::add);
 
-        // 鏋勯�犱富琛ㄦ洿鏂板璞�
+        // 浠呮洿鏂伴噾棰濆垪锛岄伩鍏嶆暣瀹炰綋 updateById 鎶婂叾瀹冮潪绌哄瓧娈碉紙濡� contract_type 榛樿鍊� 1锛変竴骞惰鐩�
+        UpdateWrapper<S> wrapper = Wrappers.update();
+        wrapper.eq("id", mainId)
+                .set("contract_amount", totalAmount);
+        // 瀹為檯鍚堝悓閲戦鍒濆鍊肩瓑浜庡悎鍚岄噾棰�
+        boolean hasNetAmount;
         try {
-            S entity = mainEntityClass.getDeclaredConstructor().newInstance();
-            Field idField = mainEntityClass.getDeclaredField("id");
-            idField.setAccessible(true);
-            idField.set(entity, mainId);
-
-            Field amountField = mainEntityClass.getDeclaredField("contractAmount");
-            amountField.setAccessible(true);
-            amountField.set(entity, totalAmount);
-
-            mainMapper.updateById(entity);
-        } catch (Exception e) {
-            throw new RuntimeException("鍔ㄦ�佹洿鏂颁富琛ㄩ噾棰濆け璐�", e);
+            mainEntityClass.getDeclaredField("netContractAmount");
+            hasNetAmount = true;
+        } catch (NoSuchFieldException e) {
+            hasNetAmount = false;
         }
+        if (hasNetAmount) {
+            wrapper.set("net_contract_amount", totalAmount);
+        }
+        mainMapper.update(null, wrapper);
     }
 
     // 鍐呴儴绫荤敤浜庡瓨鍌ㄨ仛鍚堢粨鏋�

--
Gitblit v1.9.3