Implementing Self-Media Article Management System

Implementing Self-Media Article Management System

Objectives

  • Implement self-media article listing functionality
  • Enable article publishing capabilities
  • Support article retrieval by ID
  • Implement article deletion
  • Add article publish/unpublish features

1. Self-Media Article Listing

1.1 Requirements Analysis

1.2 Database Schema and Entity Classes

The content_articles table stores self-media article information.

Article Entity Class

package com.example.model.media.entities;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import org.apache.ibatis.type.Alias;

import java.io.Serializable;
import java.util.Date;

/**
 * Self-media article content information entity
 */
@Data
@TableName("content_articles")
public class ContentArticle implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * Primary key
     */
    @TableId(value = "id", type = IdType.AUTO)
    private Integer id;

    /**
     * Content creator ID
     */
    @TableField("creator_id")
    private Integer creatorId;

    /**
     * Article title
     */
    @TableField("title")
    private String title;

    /**
     * Article content
     */
    @TableField("content")
    private String content;

    /**
     * Article layout type
     * 0 - No image article
     * 1 - Single image article
     * 3 - Multiple image article
     */
    @TableField("layout_type")
    private Short layoutType;

    /**
     * Content channel ID
     */
    @TableField("channel_id")
    private Integer channelId;

    @TableField("tags")
    private String tags;

    /**
     * Creation time
     */
    @TableField("created_time")
    private Date createdTime;

    /**
     * Submission time
     */
    @TableField("submitted_time")
    private Date submittedTime;

    /**
     * Current status
     * 0 - Draft
     * 1 - Submitted (pending review)
     * 2 - Review failed
     * 3 - Manual review
     * 4 - Manual review approved
     * 8 - Approved (pending publication)
     * 9 - Published
     */
    @TableField("status")
    private Short status;

    /**
     * Scheduled publication time (empty if not scheduled)
     */
    @TableField("publish_time")
    private Date publishTime;

    /**
     * Rejection reason
     */
    @TableField("rejection_reason")
    private String rejectionReason;

    /**
     * Published article ID
     */
    @TableField("published_id")
    private Integer publishedId;

    /**
     * Images separated by commas
     */
    @TableField("images")
    private String images;

    @TableField("is_active")
    private Short isActive;
    
    // Status enumeration
    @Alias("ArticleStatus")
    public enum Status{
        DRAFT((short)0), SUBMITTED((short)1), REJECTED((short)2), MANUAL_REVIEW((short)3), 
        MANUAL_APPROVED((short)4), APPROVED((short)8), PUBLISHED((short)9);
        short code;
        Status(short code){
            this.code = code;
        }
        public short getCode(){
            return this.code;
        }
    }

}

1.3 Interface Definition

Define interfaces in the API project example-leadnews-apis:

package com.example.api.content;

import com.example.model.common.dtos.ResponseResult;

/**
 * Content article interface
 */
public interface ArticleControllerApi {

    /**
     * Paginated search with filters for content articles
     * @param searchRequest
     * @return
     */
    public ResponseResult findAll(ArticleSearchRequest searchRequest);
}

ArticleSearchRequest

package com.example.model.media.dtos;

import com.example.model.common.dtos.PageRequestDto;
import lombok.Data;

import java.util.Date;

@Data
public class ArticleSearchRequest extends PageRequestDto {

    private Short status; // Status
    private Date startDate; // Start date
    private Date endDate; // End date
    private Integer channelId; // Channel ID
    private String keyword; // Search keyword
}

1.4 Mapper Implementation

Create com.example.content.mapper.ArticleMapper:

package com.example.content.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.model.media.entities.ContentArticle;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface ArticleMapper extends BaseMapper<ContentArticle> {
}

1.5 Service Implementation

Define ArticleService interface: com.example.content.service.ArticleService

package com.example.content.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.example.model.common.dtos.ResponseResult;
import com.example.model.media.dtos.ArticleSearchRequest;
import com.example.model.media.entities.ContentArticle;

public interface ArticleService extends IService<ContentArticle> {

    /**
     * Search all content articles
     * @return
     */
    public ResponseResult findAll(ArticleSearchRequest searchRequest);
}

Define implementation class com.example.content.service.impl.ArticleServiceImpl:

package com.example.content.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.model.common.dtos.PageResponseResult;
import com.example.model.common.dtos.ResponseResult;
import com.example.model.common.enums.AppHttpCodeEnum;
import com.example.model.content.dtos.ArticleSearchRequest;
import com.example.model.content.entities.ContentArticle;
import com.example.model.content.entities.ContentUser;
import com.example.utils.threadlocal.ContentThreadLocalUtils;
import com.example.content.mapper.ArticleMapper;
import com.example.content.service.ArticleService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@Transactional
public class ArticleServiceImpl extends ServiceImpl implements ArticleService {

    @Value("${file.server.url}")
    private String fileServerUrl;

    @Override
    public ResponseResult findAll(ArticleSearchRequest searchRequest) {
        //1. Parameter validation
        if(searchRequest == null){
            return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID);
        }
        // Pagination parameter validation
        searchRequest.validateParams();

        //2. Paginated search with filters
        IPage pageParam = new Page(searchRequest.getPage(), searchRequest.getSize());

        LambdaQueryWrapper<ContentArticle> lambdaQueryWrapper = new LambdaQueryWrapper<>();
        // Status exact match
        if(searchRequest.getStatus() != null){
            lambdaQueryWrapper.eq(ContentArticle::getStatus, searchRequest.getStatus());
        }
        // Channel ID exact match
        if(null != searchRequest.getChannelId()){
            lambdaQueryWrapper.eq(ContentArticle::getChannelId, searchRequest.getChannelId());
        }

        // Date range search
        if(searchRequest.getStartDate()!=null && searchRequest.getEndDate()!=null){
            lambdaQueryWrapper.between(ContentArticle::getPublishTime, searchRequest.getStartDate(), searchRequest.getEndDate());
        }

        // Keyword search
        if(null != searchRequest.getKeyword()){
            lambdaQueryWrapper.like(ContentArticle::getTitle, searchRequest.getKeyword());
        }

        // Get current logged-in user info
        ContentUser user = ContentThreadLocalUtils.getUser();
        if(user==null){
            return ResponseResult.errorResult(AppHttpCodeEnum.NEED_LOGIN);
        }
        lambdaQueryWrapper.eq(ContentArticle::getCreatorId, user.getId());

        // Order by creation date descending
        lambdaQueryWrapper.orderByDesc(ContentArticle::getCreatedTime);

        IPage pageResult = page(pageParam, lambdaQueryWrapper);

        //3. Package and return results
        PageResponseResult responseResult = new PageResponseResult(searchRequest.getPage(), searchRequest.getSize(), (int)pageResult.getTotal());
        responseResult.setData(pageResult.getRecords());
        responseResult.setHost(fileServerUrl);
        return responseResult;
    }
}

1.6 Controller Implementation

Define controller in the content microservice: com.example.content.controller.ArticleController

package com.example.content.controller.v1;

import com.example.apis.content.ArticleControllerApi;
import com.example.model.common.dtos.ResponseResult;
import com.example.model.content.dtos.ArticleSearchRequest;
import com.example.content.service.ArticleService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/v1/articles")
public class ArticleController implements ArticleControllerApi {

    @Autowired
    private ArticleService articleService;

    @PostMapping("/list")
    @Override
    public ResponseResult findAll(@RequestBody ArticleSearchRequest searchRequest){
        return articleService.findAll(searchRequest);
    }
}

1.7 Testing

Use Postman for testing, which requires login first to obtain a token before accessing the endpoint.

2. Channel List Display

2.1 Requirements Analysis

When the article list page is loaded, it should automatically fetch and display channel data.

2.2 Interface Definition

Add a method to the ChannelControllerApi interface to query all channel data:


/**
 * Query all channels
 * @return
 */
public ResponseResult findAllChannels();

2.3 Implementation

In the admin module, modify the ChannelController class and add the following method. The mapper and service were already defined previously:


@GetMapping("/channels")
@Override
public ResponseResult findAllChannels() {
    List<Channel> channelList = channelService.list();
    return ResponseResult.okResult(channelList);
}

2.4 Modify Content Gateway

In the content gateway's yml configuration file, add the following configuration:


- id: admin
  uri: lb://leadnews-admin
  predicates:
    - Path=/admin/**
  filters:
    - StripPrefix= 1

2.5 Testing

Test using Postman or directly through the web interface.

3. Article Publishing, Editing, and Saving as Draft

3.1 Requirements Analysis

To save an article, in addition to the content_articles table, we need two additional tables:

  • content_media - Media assets table
  • article_media_relations - Article-media relationship table

3.2 Approach Analysis

  • This functionality is common for saving, editing (if ID exists), and saving as draft
  • If editing an article (ID exists), first delete all media relationships
  • If no ID, save the article
  • Associate images in content with media assets
  • Associate cover images with media assets
  • If automatic cover selection is chosen, extract images from content as cover images:
    • If content has ≤2 images, use single image cover
    • If content has ≥3 images, use multiple image cover (3 images)
    • If no images in content, use no-image cover

Sample Request Data

{
    "title":"Background of Our News Platform",
    "type":"1", // 0 - no image, 1 - single image, 3 - multiple images, -1 - auto
    "tags":"Technology News",
    "publishTime":"2023-05-15T14:30:00.000Z",
    "channelId":1,
    "images":[
        "http://file-server.example.com/group1/M00/00/00/wKjIgl5swbGATaSAAAEPfZfx6Iw790.png"
    ],
    "status":1,
    "content":"[
    {
        \"type\":\"text\",
        \"value\":\"With the popularity of smartphones, people are more accustomed to reading news on their phones. Due to the accelerating pace of life, many people can only use fragmented time to get information, so the demand for mobile news clients is also increasing. Our news platform was developed in this context.\"
    },
    {
        \"type\":\"image\",
        \"value\":\"http://file-server.example.com/group1/M00/00/00/wKjIgl5swbGATaSAAAEPfZfx6Iw790.png\"
    }
]"
}

3.3 Interface Definition

Add a method for article submission in the interface: com.example.api.content.ArticleControllerApi


/**
 * Submit article for publication
 * @param articleDto
 * @return
 */
ResponseResult submitArticle(ArticleDto articleDto);

ArticleDto

package com.example.model.media.dtos;

import lombok.Data;

import java.util.Date;
import java.util.List;

@Data
public class ArticleDto {
    
    private Integer id;
    
    /**
     * Article title
     */
    private String title;
    
    /**
     * Channel ID
     */
    private Integer channelId;
    
    /**
     * Tags
     */
    private String tags;
    
    /**
     * Publication time
     */
    private Date publishTime;
    
    /**
     * Article content
     */
    private String content;
    
    /**
     * Cover type: 0 - no image, 1 - single image, 3 - multiple images, -1 - auto
     */
    private Short coverType;
    
    /**
     * Active status: 0 - inactive, 1 - active
     */
    private Short isActive;
    
    /**
     * Submission time
     */
    private Date submittedTime; 
    
    /**
     * Status: 1 - submitted, 0 - draft
     */
    private Short status;
    
    /**
     * Rejection reason
     */
    private String rejectionReason; 
    
    /**
     * Cover image list (multiple images separated by commas)
     */
    private List<String> coverImages;
}

3.4 Mapper Definition

Modify ArticleMediaMapper to add a method for batch insertion, used to associate media assets with articles:


void saveRelationsByContent(@Param("mediaUrls") List<String> mediaUrls, @Param("articleId") Integer articleId, @Param("relationType") int relationType);

Create new file resources/mapper/ArticleMediaMapper.xml:




<mapper namespace="com.example.content.mapper.ArticleMediaMapper">
    <insert id="saveRelationsByContent">
        insert into article_media_relations (media_id, article_id, relation_type, sort_order)
        values
        <foreach item="mediaUrl" index="sortOrder" collection="mediaUrls" separator="," >
            (#{mediaUrl}, #{articleId}, #{relationType}, #{sortOrder})
        </foreach>
    </insert>
</mapper>

3.5 Business Layer Code

Prepare constants class: com.example.common.constants.content.ContentConstants


package com.example.common.constants.content;

public class ContentConstants {

    public static final Short COLLECT_MEDIA = 1; // Collect
    public static final Short CANCEL_COLLECT_MEDIA = 0; // Cancel collection

    public static final String CONTENT_TYPE_IMAGE = "image";

    public static final Short COVER_TYPE_NONE = 0;
    public static final Short COVER_TYPE_SINGLE = 1;
    public static final Short COVER_TYPE_MULTIPLE = 3;
    public static final Short COVER_TYPE_AUTO = -1;

    public static final Short RELATION_CONTENT = 0;
    public static final Short RELATION_COVER = 1;
}

Add method to ArticleService class:


/**
  * Publish or save draft of content article
  * @param articleDto
  * @param isSubmit  1 for submission, 0 for draft
  * @return
 */
ResponseResult saveArticle(ArticleDto articleDto, Short isSubmit);

Implementation class:


@Override
public ResponseResult saveArticle(ArticleDto articleDto, Short isSubmit) {
    //1. Parameter validation
    if (articleDto == null || StringUtils.isBlank(articleDto.getContent())) {
        return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID);
    }

    //2. Save or update article
    ContentArticle article = new ContentArticle();
    BeanUtils.copyProperties(articleDto, article);
    if (ContentConstants.COVER_TYPE_AUTO.equals(articleDto.getCoverType())) {
        article.setCoverType(null);
    }
    if (articleDto.getCoverImages() != null && articleDto.getCoverImages().size() > 0) {
        //[image1.jpg,image2.jpg]
        article.setImages(articleDto.getCoverImages().toString().replace("[", ")
                         .replace("]", "").replace(fileServerUrl, ")
                         .replace(" ", ""));
    }
    // Save or update article
    saveOrUpdateArticle(article, isSubmit);

    //3. Associate article with media assets
    String content = articleDto.getContent();
    List<Map> contentElements = JSON.parseArray(content, Map.class);
    List<String> mediaUrls = extractMediaUrls(contentElements);

    //3.1 Associate images in content with media assets
    if (isSubmit == ContentArticle.Status.SUBMITTED.getCode() && mediaUrls.size() != 0) {
        ResponseResult responseResult = saveContentRelations(mediaUrls, article.getId());
        if (responseResult != null) {
            return responseResult;
        }
    }

    //3.2 Associate cover images with media assets, set article cover type to auto
    if (isSubmit == ContentArticle.Status.SUBMITTED.getCode()) {
        ResponseResult responseResult = saveCoverRelations(articleDto, mediaUrls, article);
        if (responseResult != null) {
            return responseResult;
        }
    }

    return ResponseResult.okResult(AppHttpCodeEnum.SUCCESS);
}

/**
     * Set cover image relationships
     *
     * @param articleDto
     * @param mediaUrls
     * @param article
     * @return
     */
private ResponseResult saveCoverRelations(ArticleDto articleDto, List<String> mediaUrls, ContentArticle article) {
    List<String> coverImages = articleDto.getCoverImages();
    // Auto match cover
    if (articleDto.getCoverType().equals(ContentConstants.COVER_TYPE_AUTO)) {
        // Content has ≤2 images, set as single image
        if (mediaUrls.size() > 0 && mediaUrls.size() <= 2) {
            article.setCoverType(ContentConstants.COVER_TYPE_SINGLE);
            coverImages = mediaUrls.stream().limit(1).collect(Collectors.toList());
        } else if (mediaUrls.size() > 2) {
            // Content has >2 images, set as multiple images
            article.setCoverType(ContentConstants.COVER_TYPE_MULTIPLE);
            coverImages = mediaUrls.stream().limit(3).collect(Collectors.toList());
        } else {
            // No images in content, set as no image
            article.setCoverType(ContentConstants.COVER_TYPE_NONE);
        }
        // Update article information
        if (coverImages != null && coverImages.size() > 0) {
            article.setImages(coverImages.toString().replace("[", ")
                             .replace("]", "").replace(fileServerUrl, ")
                             .replace(" ", ""));
        }
        updateById(article);
    }
    // Save cover image relationships
    if (coverImages != null && coverImages.size() > 0) {
        ResponseResult responseResult = saveCoverImageRelations(coverImages, article.getId());
        if (responseResult != null) {
            return responseResult;
        }
    }
    return null;
}

/**
     * @param coverImages
     * @param articleId
     * @return
     */
private ResponseResult saveCoverImageRelations(List<String> coverImages, Integer articleId) {
    List<String> mediaUrls = new ArrayList<>();
    for (String image : coverImages) {
        mediaUrls.add(image.replace(fileServerUrl,""));
    }

    return saveMediaRelations(mediaUrls, articleId, ContentConstants.RELATION_COVER);
}

/**
     * Save relationships between media assets and article content
     *
     * @param mediaUrls
     * @param articleId
     * @return
     */
private ResponseResult saveContentRelations(List<String> mediaUrls, Integer articleId) {
    return saveMediaRelations(mediaUrls, articleId, ContentConstants.RELATION_CONTENT);
}

@Autowired
private MediaMapper mediaMapper;

/**
     * Save media relationships
     *
     * @param mediaUrls
     * @param articleId
     * @param relationType
     * @return
     */
private ResponseResult saveMediaRelations(List<String> mediaUrls, Integer articleId, Short relationType) {
    //1. Get media asset information from database
    LambdaQueryWrapper<Media> lambdaQueryWrapper = new LambdaQueryWrapper<>();
    lambdaQueryWrapper.in(Media::getUrl, mediaUrls);
    lambdaQueryWrapper.eq(Media::getUserId, ContentThreadLocalUtils.getUser().getId());
    List<Media> dbMedia = mediaMapper.selectList(lambdaQueryWrapper);
    //2. Get media IDs from URLs
    List<String> mediaIds = new ArrayList<>();
    if (dbMedia != null && dbMedia.size() > 0) {
        Map urlIdMap = dbMedia.stream().collect(Collectors.toMap(Media::getUrl, Media::getId));
        for (String url : mediaUrls) {
            String mediaId = String.valueOf(urlIdMap.get(url));
            // Not found
            if ("null".equals(mediaId)) {
                return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID, "Image asset is invalid");
            }
            // Found
            mediaIds.add(mediaId);
        }
    }

    //3. Batch save data
    articleMediaMapper.saveRelations(mediaIds, articleId, relationType);
    return null;
}

/**
     * Extract media URLs from content
     *
     * @param contentElements
     * @return
     */
private List<String> extractMediaUrls(List<Map> contentElements) {
    List<String> mediaUrls = new ArrayList<>();
    for (Map element : contentElements) {
        if (element.get("type").equals(ContentConstants.CONTENT_TYPE_IMAGE)) {
            String imgUrl = (String) element.get("value");
            imgUrl = imgUrl.replace(fileServerUrl, "");
            mediaUrls.add(imgUrl);
        }
    }
    return mediaUrls;
}

@Autowired
private ArticleMediaMapper articleMediaMapper;

/**
     * Save or update article
     *
     * @param article
     * @param isSubmit
     */
private void saveOrUpdateArticle(ContentArticle article, Short isSubmit) {
    article.setStatus(isSubmit);
    article.setCreatorId(ContentThreadLocalUtils.getUser().getId());
    article.setCreatedTime(new Date());
    article.setSubmittedTime(new Date());
    article.setIsActive((short) 1);
    if (article.getId() == null) {
        save(article);
    } else {
        // If updating, first delete media relationships
        LambdaQueryWrapper<ArticleMedia> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(ArticleMedia::getArticleId, article.getId());
        articleMediaMapper.delete(queryWrapper);
        updateById(article);
    }
}

3.6 Controller Layer

Add method to ArticleController class:


@PostMapping("/submit")
@Override
public ResponseResult submitArticle(@RequestBody ArticleDto articleDto) {
    if(articleDto.getStatus() == ContentArticle.Status.SUBMITTED.getCode()){
        // Submit article
        return articleService.saveArticle(articleDto, ContentArticle.Status.SUBMITTED.getCode());
    }else{
        // Save as draft
        return articleService.saveArticle(articleDto, ContentArticle.Status.DRAFT.getCode());
    }
}

3.7 Testing

Test using Postman or integrate with frontend application.

4. Query Article by ID

4.1 Requirements Analysis

When editing an article, retrieve the article data by ID to display in the edit page.

4.2 Interface Definition

Add method in ArticleControllerApi interface:


/**
     * Get article information by ID
     * @return
     */
ResponseResult findArticleById(Integer id);

4.3 Business Layer Implementation

Add method to ArticleService interface to query article by ID:


/**
     * Query article by ID
     * @return
     */
ResponseResult findArticleById(Integer id);

Implementation class:


@Override
public ResponseResult findArticleById(Integer id) {
    //1. Parameter validation
    if(id == null){
        return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID, "Article ID is required");
    }
    //2. Query data
    ContentArticle article = getById(id);
    if(article == null){
        return ResponseResult.errorResult(AppHttpCodeEnum.DATA_NOT_FOUND, "Article does not exist");
    }

    //3. Return result
    ResponseResult responseResult = ResponseResult.okResult(article);
    responseResult.setHost(fileServerUrl);
    return responseResult;
}

4.4 Controller Layer

Add query method to ArticleController:


@GetMapping("/one/{id}")
@Override
public ResponseResult findArticleById(@PathVariable("id") Integer id) {
    return articleService.findArticleById(id);
}

4.5 Testing

Test through the web interface.

5. Article Deletion

5.1 Requirements Analysis

  • Articles with status 9 (published) and active status 1 (published) cannot be deleted. Deactivated articles can be deleted.
  • Before deleting an article, media relationships must be removed first.

5.2 Interface Definition

Add delete method to ArticleControllerApi:


/**
     * Delete article
     * @return
     */
ResponseResult deleteArticle(Integer id);

5.3 Business Layer

Add delete method to ArticleService:


/**
     * Delete article
     * @return
     */
ResponseResult deleteArticle(Integer id);

Implementation class:


@Override
public ResponseResult deleteArticle(Integer id) {
    //1. Parameter validation
    if(id == null){
        return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID, "Article ID is required");
    }
    //2. Get article data
    ContentArticle article = getById(id);
    if(article == null){
        return ResponseResult.errorResult(AppHttpCodeEnum.DATA_NOT_FOUND, "Article does not exist");
    }

    //3. Check article status - status=9 and isActive=1
    if(article.getStatus().equals(ContentArticle.Status.PUBLISHED.getCode()) && article.getIsActive().equals(ContentConstants.CONTENT_ACTIVE)){
        return ResponseResult.errorResult(AppHttpCodeEnum.OPERATION_FORBIDDEN, "Cannot delete published article");
    }

    //4. Remove media relationships
    articleMediaMapper.delete(Wrappers.<ArticleMedia>lambdaQuery().eq(ArticleMedia::getArticleId, article.getId()));

    //5. Delete article
    removeById(article.getId());
    return ResponseResult.okResult(AppHttpCodeEnum.SUCCESS);
}

5.4 Controller Layer

Add delete method to ArticleController:


@GetMapping("/delete/{id}")
@Override
public ResponseResult deleteArticle(@PathVariable("id") Integer id) {
    return articleService.deleteArticle(id);
}

6. Article Publishing and Unpublishing

6.1 Requirements Analysis

  • Published articles (status 9) can be published (isActive = 1) or unpublished (isActive = 0)
  • When publishing/unpublishing, article configuration in the app should be synchronized (not implemented yet, will be added later when covering article review)

6.2 Interface Definition

Add method to ArticleControllerApi:


 /**
     * Publish or unpublish article
     * @param articleDto
     * @return
     */
ResponseResult publishOrUnpublish(ArticleDto articleDto);

6.3 Business Layer

Add method to ArticleService:


/**
     * Publish or unpublish article
     * @param articleDto
     * @return
     */
ResponseResult publishOrUnpublish(ArticleDto articleDto);

Implementation class:


@Override
public ResponseResult publishOrUnpublish(ArticleDto articleDto) {
    //1. Parameter validation
    if(articleDto == null || articleDto.getId() == null){
        return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID);
    }

    //2. Query article
    ContentArticle article = getById(articleDto.getId());
    if(article == null){
        return ResponseResult.errorResult(AppHttpCodeEnum.DATA_NOT_FOUND, "Article does not exist");
    }

    //3. Check if article is published
    if(!article.getStatus().equals(ContentArticle.Status.PUBLISHED.getCode())){
        return ResponseResult.errorResult(AppHttpCodeEnum.OPERATION_FORBIDDEN, "Only published articles can be published or unpublished");
    }

    //4. Update article status, sync to app (TODO for later implementation)
    if(articleDto.getIsActive() != null && articleDto.getIsActive() > -1 && articleDto.getIsActive() < 2){
        update(Wrappers.<ContentArticle>lambdaUpdate()
               .eq(ContentArticle::getId, articleDto.getId())
               .set(ContentArticle::getIsActive, articleDto.getIsActive()));
    }
    return ResponseResult.okResult(AppHttpCodeEnum.SUCCESS);
}

6.4 Controller Layer


@PostMapping("/publish_or_unpublish")
@Override
public ResponseResult publishOrUnpublish(@RequestBody ArticleDto articleDto) {
    return articleService.publishOrUnpublish(articleDto);
}

6.5 Testing

Tags: self-media article management content publishing java Spring Boot

Posted on Sun, 18 Oct 2026 16:25:13 +0000 by abcd1234